From 73a956a09bbb200e73fbb477e5875a9216d1c63c Mon Sep 17 00:00:00 2001 From: Vijay Janapa Reddi Date: Sun, 15 Feb 2026 14:03:27 -0500 Subject: [PATCH] chore(volumes,vscode-ext): batch volume updates and tooling improvements Checkpoint the branch-wide content/config revisions together with workbench enhancements so chapter rendering and developer workflows stay aligned. This captures the current validation-driven formatting and parallel build/debug improvements in one commit. --- book/cli/commands/validate.py | 25 + book/quarto/_quarto.yml | 2 +- book/quarto/config/_quarto-epub-vol1.yml | 72 ++- book/quarto/config/_quarto-epub-vol2.yml | 8 +- book/quarto/config/_quarto-html-vol1.yml | 8 +- book/quarto/config/_quarto-html-vol2.yml | 8 +- .../config/_quarto-pdf-vol1-copyedit.yml | 7 +- book/quarto/config/_quarto-pdf-vol1.yml | 57 +-- .../config/_quarto-pdf-vol2-copyedit.yml | 7 +- book/quarto/config/_quarto-pdf-vol2.yml | 7 +- book/quarto/config/shared/README.md | 1 + book/quarto/config/shared/base/diagram.yml | 1 + .../quarto/config/shared/base/execute-env.yml | 7 + .../shared/pdf/build-copyedit-common.yml | 8 +- .../shared/pdf/build-production-common.yml | 5 +- .../vol1/backmatter/appendix_algorithm.qmd | 16 +- .../vol1/backmatter/appendix_assumptions.qmd | 8 +- .../contents/vol1/backmatter/appendix_dam.qmd | 4 +- .../vol1/backmatter/appendix_data.qmd | 6 +- .../vol1/backmatter/appendix_machine.qmd | 80 +-- .../vol1/backmatter/glossary/glossary.qmd | 4 +- .../contents/vol1/backmatter/references.bib | 56 +++ .../vol1/benchmarking/benchmarking.qmd | 171 ++++--- .../contents/vol1/conclusion/conclusion.qmd | 12 +- .../data_engineering/data_engineering.qmd | 111 ++-- .../vol1/data_selection/data_selection.qmd | 145 +++--- .../contents/vol1/frameworks/frameworks.qmd | 164 +++--- .../contents/vol1/frontmatter/about.qmd | 8 +- .../vol1/hw_acceleration/hw_acceleration.qmd | 175 +++---- .../vol1/introduction/introduction.qmd | 26 +- book/quarto/contents/vol1/ml_ops/ml_ops.qmd | 32 +- .../contents/vol1/ml_systems/ml_systems.qmd | 110 ++-- .../contents/vol1/ml_workflow/ml_workflow.qmd | 19 +- .../vol1/model_serving/model_serving.qmd | 199 ++++---- .../images/png/comm_primitives.png | Bin 0 -> 319119 bytes .../nn_architectures/nn_architectures.qmd | 131 ++--- .../vol1/nn_computation/nn_computation.qmd | 153 +++--- .../vol1/optimizations/model_compression.qmd | 174 +++---- .../vol1/parts/optimize_principles.qmd | 4 +- .../responsible_engr/responsible_engr.qmd | 50 +- .../contents/vol1/training/training.qmd | 475 +++++++++--------- book/quarto/contents/vol2/ai_good/ai_good.qmd | 54 +- .../vol2/backmatter/glossary/glossary.qmd | 2 +- .../communication_ops/communication_ops.qmd | 32 +- .../contents/vol2/conclusion/conclusion.qmd | 4 +- .../distributed_training.qmd | 30 +- .../edge_intelligence/edge_intelligence.qmd | 116 ++--- .../emerging_challenges.qmd | 82 +-- .../vol2/fault_tolerance/fault_tolerance.qmd | 12 +- .../contents/vol2/inference/inference.qmd | 98 ++-- .../vol2/infrastructure/infrastructure.qmd | 20 +- .../vol2/introduction/introduction.qmd | 2 +- .../contents/vol2/ops_scale/ops_scale.qmd | 62 +-- .../optimization/_shelved_automl_section.qmd | 2 +- .../vol2/parts/inference_principles.qmd | 2 +- .../vol2/parts/production_principles.qmd | 4 +- .../contents/vol2/parts/scale_principles.qmd | 2 +- .../vol2/responsible_ai/responsible_ai.qmd | 22 +- .../contents/vol2/robust_ai/robust_ai.qmd | 48 +- .../security_privacy/security_privacy.qmd | 18 +- book/quarto/contents/vol2/storage/storage.qmd | 96 ++-- .../vol2/sustainable_ai/sustainable_ai.qmd | 154 +++--- .../scripts/mit_press/FIGURE_LIST_VOL1.csv | 2 +- book/quarto/tex/header-includes.tex | 158 ++---- book/tools/scripts/gen_bio_nn_svg.py | 188 +++++++ .../scripts/genai/generate_comm_primitives.py | 153 ++++++ .../genai/generate_comm_primitives_diagram.py | 153 ++++++ book/tools/scripts/genai/replace_tikz.py | 45 ++ book/vscode-ext/README.md | 17 + book/vscode-ext/package.json | 17 +- book/vscode-ext/src/commands/buildCommands.ts | 38 ++ .../src/commands/contextMenuCommands.ts | 7 +- book/vscode-ext/src/commands/debugCommands.ts | 143 +----- .../src/commands/precommitCommands.ts | 23 +- book/vscode-ext/src/extension.ts | 9 +- .../src/providers/buildTreeProvider.ts | 18 +- .../src/providers/configTreeProvider.ts | 1 + .../src/providers/debugTreeProvider.ts | 10 +- .../src/providers/precommitTreeProvider.ts | 55 +- book/vscode-ext/src/utils/chapters.ts | 41 +- book/vscode-ext/src/utils/parallelDebug.ts | 13 +- .../vscode-ext/src/utils/quartoConfigReset.ts | 25 + book/vscode-ext/src/utils/terminal.ts | 8 +- .../src/validation/precommitStatusManager.ts | 47 ++ current_plot.png | Bin 0 -> 47272 bytes proposed_plot.png | Bin 0 -> 48542 bytes 86 files changed, 2609 insertions(+), 1980 deletions(-) create mode 100644 book/quarto/config/shared/base/execute-env.yml create mode 100644 book/quarto/contents/vol1/nn_architectures/images/png/comm_primitives.png create mode 100644 book/tools/scripts/gen_bio_nn_svg.py create mode 100644 book/tools/scripts/genai/generate_comm_primitives.py create mode 100644 book/tools/scripts/genai/generate_comm_primitives_diagram.py create mode 100644 book/tools/scripts/genai/replace_tikz.py create mode 100644 book/vscode-ext/src/utils/quartoConfigReset.ts create mode 100644 book/vscode-ext/src/validation/precommitStatusManager.ts create mode 100644 current_plot.png create mode 100644 proposed_plot.png diff --git a/book/cli/commands/validate.py b/book/cli/commands/validate.py index 74332c420b..c6124a651d 100644 --- a/book/cli/commands/validate.py +++ b/book/cli/commands/validate.py @@ -1141,6 +1141,31 @@ class ValidateCommand: ) ) + # Missing blank line before footnote definition + # Pandoc requires footnote definitions to start a new block. + # Without a preceding blank line, Pandoc treats the definition + # as continuation text and renders [^fn-name] as literal text. + fn_def_line_pat = re.compile(r"^\[\^[^\]]+\]:") + for idx, line in enumerate(lines): + if fn_def_line_pat.match(line) and idx > 0: + prev = lines[idx - 1] + if prev.strip(): # previous line is not blank + fn_match = re.match(r"^\[\^([^\]]+)\]:", line) + fn_id_str = fn_match.group(1) if fn_match else "?" + issues.append( + ValidationIssue( + file=self._relative_file(file), + line=idx + 1, + code="footnote_missing_blank_line", + message=( + f"Footnote definition [^{fn_id_str}] has no blank line before it — " + f"Pandoc will not parse it as a footnote" + ), + severity="error", + context=f"prev: {prev.strip()[:60]}", + ) + ) + return ValidationRunResult( name="footnote-refs", description="Validate footnote references and definitions", diff --git a/book/quarto/_quarto.yml b/book/quarto/_quarto.yml index 7243cc7686..cec4f41155 120000 --- a/book/quarto/_quarto.yml +++ b/book/quarto/_quarto.yml @@ -1 +1 @@ -config/_quarto-epub-vol1.yml \ No newline at end of file +config/_quarto-pdf-vol1.yml \ No newline at end of file diff --git a/book/quarto/config/_quarto-epub-vol1.yml b/book/quarto/config/_quarto-epub-vol1.yml index ed8a456775..4a77aa3732 100644 --- a/book/quarto/config/_quarto-epub-vol1.yml +++ b/book/quarto/config/_quarto-epub-vol1.yml @@ -20,10 +20,6 @@ project: browser: false navigate: false -execute: - env: - PYTHONPATH: "." - book: favicon: assets/images/icons/favicon.png cover-image: assets/images/covers/cover-hardcover-book-vol1.png @@ -63,56 +59,53 @@ book: - index.qmd # Volume I Frontmatter - # - contents/vol1/frontmatter/about.qmd - # - contents/vol1/frontmatter/acknowledgements.qmd + - contents/vol1/frontmatter/about.qmd + - contents/vol1/frontmatter/acknowledgements.qmd # Part I: Foundations - # - part: "Foundations" - # - contents/vol1/parts/foundations_principles.qmd - # - contents/vol1/introduction/introduction.qmd - # - contents/vol1/ml_systems/ml_systems.qmd - # - contents/vol1/ml_workflow/ml_workflow.qmd - # - contents/vol1/data_engineering/data_engineering.qmd + - part: "Foundations" + - contents/vol1/parts/foundations_principles.qmd + - contents/vol1/introduction/introduction.qmd + - contents/vol1/ml_systems/ml_systems.qmd + - contents/vol1/ml_workflow/ml_workflow.qmd + - contents/vol1/data_engineering/data_engineering.qmd # Part II: Build - # - part: "Build" - # - contents/vol1/parts/build_principles.qmd - # - contents/vol1/nn_computation/nn_computation.qmd - # - contents/vol1/nn_architectures/nn_architectures.qmd - # - contents/vol1/frameworks/frameworks.qmd - # - contents/vol1/training/training.qmd + - part: "Build" + - contents/vol1/parts/build_principles.qmd + - contents/vol1/nn_computation/nn_computation.qmd + - contents/vol1/nn_architectures/nn_architectures.qmd + - contents/vol1/frameworks/frameworks.qmd + - contents/vol1/training/training.qmd # Part III: Optimize - part: "Optimize" - # - contents/vol1/parts/optimize_principles.qmd + - contents/vol1/parts/optimize_principles.qmd - contents/vol1/data_selection/data_selection.qmd - # - contents/vol1/optimizations/model_compression.qmd - # - contents/vol1/hw_acceleration/hw_acceleration.qmd - # - contents/vol1/benchmarking/benchmarking.qmd + - contents/vol1/optimizations/model_compression.qmd + - contents/vol1/hw_acceleration/hw_acceleration.qmd + - contents/vol1/benchmarking/benchmarking.qmd # Part IV: Deploy - # - part: "Deploy" - # - contents/vol1/parts/deploy_principles.qmd - # - contents/vol1/model_serving/model_serving.qmd - # - contents/vol1/ml_ops/ml_ops.qmd - # - contents/vol1/responsible_engr/responsible_engr.qmd - # - contents/vol1/conclusion/conclusion.qmd - # - contents/vol1/backmatter/references.qmd + - part: "Deploy" + - contents/vol1/parts/deploy_principles.qmd + - contents/vol1/model_serving/model_serving.qmd + - contents/vol1/ml_ops/ml_ops.qmd + - contents/vol1/responsible_engr/responsible_engr.qmd + - contents/vol1/conclusion/conclusion.qmd + - contents/vol1/backmatter/references.qmd - # - part: "Appendices" - # chapters: - # - contents/vol1/backmatter/appendix_dam.qmd - # - contents/vol1/backmatter/appendix_machine.qmd - # - contents/vol1/backmatter/appendix_algorithm.qmd - # - contents/vol1/backmatter/appendix_data.qmd - # - contents/vol1/backmatter/glossary/glossary.qmd + - part: "Appendices" + chapters: + - contents/vol1/backmatter/appendix_dam.qmd + - contents/vol1/backmatter/appendix_machine.qmd + - contents/vol1/backmatter/appendix_algorithm.qmd + - contents/vol1/backmatter/appendix_data.qmd + - contents/vol1/backmatter/glossary/glossary.qmd bibliography: - contents/vol1/backmatter/references.bib -editor: - render-on-save: true - format: epub: language: "en-US" @@ -143,6 +136,7 @@ format: metadata-files: - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/epub/filters.yml - config/shared/epub/filter-metadata.yml diff --git a/book/quarto/config/_quarto-epub-vol2.yml b/book/quarto/config/_quarto-epub-vol2.yml index e73b9ec017..c0f51f2d46 100644 --- a/book/quarto/config/_quarto-epub-vol2.yml +++ b/book/quarto/config/_quarto-epub-vol2.yml @@ -20,10 +20,6 @@ project: browser: false navigate: false -execute: - env: - PYTHONPATH: "." - book: favicon: assets/images/icons/favicon.png cover-image: assets/images/covers/cover-hardcover-book-vol2.png @@ -139,9 +135,6 @@ bibliography: - contents/vol2/emerging_challenges/emerging_challenges.bib - contents/vol2/conclusion/conclusion.bib -editor: - render-on-save: true - format: epub: language: "en-US" @@ -172,6 +165,7 @@ format: metadata-files: - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/epub/filters.yml - config/shared/epub/filter-metadata.yml diff --git a/book/quarto/config/_quarto-html-vol1.yml b/book/quarto/config/_quarto-html-vol1.yml index cd2071730f..b4c201afd0 100644 --- a/book/quarto/config/_quarto-html-vol1.yml +++ b/book/quarto/config/_quarto-html-vol1.yml @@ -18,10 +18,6 @@ project: browser: false navigate: false -execute: - env: - PYTHONPATH: "." - website: title: "Introduction to Machine Learning Systems" description: "Volume I: Introduction to Machine Learning Systems. Build, optimize, and deploy machine learning systems." @@ -234,9 +230,6 @@ website: bibliography: - contents/vol1/backmatter/references.bib -editor: - render-on-save: true - format: html: lightbox: true @@ -296,6 +289,7 @@ format: metadata-files: - config/shared/base/crossref-video.yml - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/html/filters.yml - config/shared/html/filter-metadata.yml diff --git a/book/quarto/config/_quarto-html-vol2.yml b/book/quarto/config/_quarto-html-vol2.yml index 1f7ccb4836..4632bac0ca 100644 --- a/book/quarto/config/_quarto-html-vol2.yml +++ b/book/quarto/config/_quarto-html-vol2.yml @@ -18,10 +18,6 @@ project: browser: false navigate: false -execute: - env: - PYTHONPATH: "." - website: title: "Advanced Machine Learning Systems" description: "Volume II: Advanced Machine Learning Systems. Scale, distribute, and govern machine learning systems in production." @@ -232,9 +228,6 @@ bibliography: - contents/vol2/ai_good/ai_good.bib - contents/vol2/emerging_challenges/emerging_challenges.bib -editor: - render-on-save: true - format: html: lightbox: true @@ -294,6 +287,7 @@ format: metadata-files: - config/shared/base/crossref-video.yml - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/html/filters.yml - config/shared/html/filter-metadata.yml diff --git a/book/quarto/config/_quarto-pdf-vol1-copyedit.yml b/book/quarto/config/_quarto-pdf-vol1-copyedit.yml index f6553c2f59..b5826edc80 100644 --- a/book/quarto/config/_quarto-pdf-vol1-copyedit.yml +++ b/book/quarto/config/_quarto-pdf-vol1-copyedit.yml @@ -105,12 +105,10 @@ book: bibliography: - contents/vol1/backmatter/references.bib -citation: true -license: CC-BY-NC-SA - metadata-files: - config/shared/base/crossref-video.yml - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/pdf/filters.yml - config/shared/pdf/filter-metadata.yml @@ -122,9 +120,6 @@ metadata-files: - config/shared/pdf/copyedit-watermark.yml - config/shared/pdf/build-copyedit-common.yml -editor: - render-on-save: false - format: titlepage-pdf: # Simplified cover for copyedit version diff --git a/book/quarto/config/_quarto-pdf-vol1.yml b/book/quarto/config/_quarto-pdf-vol1.yml index 7ff6b43172..9beb148cf6 100644 --- a/book/quarto/config/_quarto-pdf-vol1.yml +++ b/book/quarto/config/_quarto-pdf-vol1.yml @@ -50,56 +50,56 @@ book: # ================================================== # Volume I Frontmatter # ================================================== - # - contents/vol1/frontmatter/about.qmd - # - contents/vol1/frontmatter/acknowledgements.qmd - # - contents/vol1/frontmatter/notation.qmd + - contents/vol1/frontmatter/about.qmd + - contents/vol1/frontmatter/acknowledgements.qmd + - contents/vol1/frontmatter/notation.qmd # ================================================== # Part I: Foundations # ================================================== - # - contents/vol1/parts/foundations_principles.qmd + - contents/vol1/parts/foundations_principles.qmd - contents/vol1/introduction/introduction.qmd - # - contents/vol1/ml_systems/ml_systems.qmd - # - contents/vol1/ml_workflow/ml_workflow.qmd - # - contents/vol1/data_engineering/data_engineering.qmd + - contents/vol1/ml_systems/ml_systems.qmd + - contents/vol1/ml_workflow/ml_workflow.qmd + - contents/vol1/data_engineering/data_engineering.qmd # ================================================== # Part II: Build # ================================================== - # - contents/vol1/parts/build_principles.qmd - # - contents/vol1/nn_computation/nn_computation.qmd - # - contents/vol1/nn_architectures/nn_architectures.qmd - # - contents/vol1/frameworks/frameworks.qmd - # - contents/vol1/training/training.qmd + - contents/vol1/parts/build_principles.qmd + - contents/vol1/nn_computation/nn_computation.qmd + - contents/vol1/nn_architectures/nn_architectures.qmd + - contents/vol1/frameworks/frameworks.qmd + - contents/vol1/training/training.qmd # ================================================== # Part III: Optimize # ================================================== - # - contents/vol1/parts/optimize_principles.qmd - # - contents/vol1/data_selection/data_selection.qmd - # - contents/vol1/optimizations/model_compression.qmd - # - contents/vol1/hw_acceleration/hw_acceleration.qmd - # - contents/vol1/benchmarking/benchmarking.qmd + - contents/vol1/parts/optimize_principles.qmd + - contents/vol1/data_selection/data_selection.qmd + - contents/vol1/optimizations/model_compression.qmd + - contents/vol1/hw_acceleration/hw_acceleration.qmd + - contents/vol1/benchmarking/benchmarking.qmd # ================================================== # Part IV: Deploy # ================================================== - # - contents/vol1/parts/deploy_principles.qmd - # - contents/vol1/model_serving/model_serving.qmd - # - contents/vol1/ml_ops/ml_ops.qmd - # - contents/vol1/responsible_engr/responsible_engr.qmd - # - contents/vol1/conclusion/conclusion.qmd - # - contents/vol1/backmatter/references.qmd + - contents/vol1/parts/deploy_principles.qmd + - contents/vol1/model_serving/model_serving.qmd + - contents/vol1/ml_ops/ml_ops.qmd + - contents/vol1/responsible_engr/responsible_engr.qmd + - contents/vol1/conclusion/conclusion.qmd + - contents/vol1/backmatter/references.qmd # ================================================== # Appendices (uses Appendix A, B, C... numbering) # ================================================== # appendices: - # - contents/vol1/backmatter/appendix_dam.qmd - # - contents/vol1/backmatter/appendix_machine.qmd - # - contents/vol1/backmatter/appendix_algorithm.qmd - # - contents/vol1/backmatter/appendix_data.qmd - # - contents/vol1/backmatter/glossary/glossary.qmd + - contents/vol1/backmatter/appendix_dam.qmd + - contents/vol1/backmatter/appendix_machine.qmd + - contents/vol1/backmatter/appendix_algorithm.qmd + - contents/vol1/backmatter/appendix_data.qmd + - contents/vol1/backmatter/glossary/glossary.qmd bibliography: - contents/vol1/backmatter/references.bib @@ -110,6 +110,7 @@ license: CC-BY-NC-SA metadata-files: - config/shared/base/crossref-video.yml - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/pdf/filters.yml - config/shared/pdf/filter-metadata.yml diff --git a/book/quarto/config/_quarto-pdf-vol2-copyedit.yml b/book/quarto/config/_quarto-pdf-vol2-copyedit.yml index 9efe6f7e5b..4c283ec12b 100644 --- a/book/quarto/config/_quarto-pdf-vol2-copyedit.yml +++ b/book/quarto/config/_quarto-pdf-vol2-copyedit.yml @@ -105,12 +105,10 @@ book: bibliography: - contents/vol2/backmatter/references.bib -citation: true -license: CC-BY-NC-SA - metadata-files: - config/shared/base/crossref-video.yml - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/pdf/filters.yml - config/shared/pdf/filter-metadata.yml @@ -122,9 +120,6 @@ metadata-files: - config/shared/pdf/copyedit-watermark.yml - config/shared/pdf/build-copyedit-common.yml -editor: - render-on-save: false - format: titlepage-pdf: # Simplified cover for copyedit version diff --git a/book/quarto/config/_quarto-pdf-vol2.yml b/book/quarto/config/_quarto-pdf-vol2.yml index 415954c65c..e98e75d7bf 100644 --- a/book/quarto/config/_quarto-pdf-vol2.yml +++ b/book/quarto/config/_quarto-pdf-vol2.yml @@ -100,12 +100,10 @@ book: bibliography: - contents/vol2/backmatter/references.bib -citation: true -license: CC-BY-NC-SA - metadata-files: - config/shared/base/crossref-video.yml - config/shared/base/custom-numbered-blocks.yml + - config/shared/base/execute-env.yml - config/shared/base/diagram.yml - config/shared/pdf/filters.yml - config/shared/pdf/filter-metadata.yml @@ -115,9 +113,6 @@ metadata-files: - config/shared/pdf/titlepage-pdf-common.yml - config/shared/pdf/build-production-common.yml -editor: - render-on-save: true - format: titlepage-pdf: coverpage: true diff --git a/book/quarto/config/shared/README.md b/book/quarto/config/shared/README.md index 3950d6528f..576c29c1d3 100644 --- a/book/quarto/config/shared/README.md +++ b/book/quarto/config/shared/README.md @@ -35,6 +35,7 @@ Later files override earlier files. - `base/crossref-video.yml` - `base/custom-numbered-blocks.yml` - `base/diagram.yml` +- `base/execute-env.yml` - `html/announcement.yml` - `html/filters.yml` - `html/filter-metadata.yml` diff --git a/book/quarto/config/shared/base/diagram.yml b/book/quarto/config/shared/base/diagram.yml index ca502cee41..861bda69a2 100644 --- a/book/quarto/config/shared/base/diagram.yml +++ b/book/quarto/config/shared/base/diagram.yml @@ -51,6 +51,7 @@ diagram: - '\definecolor{GrayLine}{HTML}{666666}' - '\definecolor{OliveL}{RGB}{230,227,191}' - '\definecolor{OliveLine}{RGB}{173,166,10}' + - '\definecolor{OrangeFill}{HTML}{FDEBD0}' - '\definecolor{OrangeL}{RGB}{250,212,175}' - '\definecolor{OrangeLine}{HTML}{E67817}' - '\definecolor{RedL}{RGB}{253,226,240}' diff --git a/book/quarto/config/shared/base/execute-env.yml b/book/quarto/config/shared/base/execute-env.yml new file mode 100644 index 0000000000..dfcc828d00 --- /dev/null +++ b/book/quarto/config/shared/base/execute-env.yml @@ -0,0 +1,7 @@ +execute: + env: + PYTHONPATH: "." + MPLBACKEND: "Agg" + +editor: + render-on-save: true diff --git a/book/quarto/config/shared/pdf/build-copyedit-common.yml b/book/quarto/config/shared/pdf/build-copyedit-common.yml index 6ea58b5d51..5462763abb 100644 --- a/book/quarto/config/shared/pdf/build-copyedit-common.yml +++ b/book/quarto/config/shared/pdf/build-copyedit-common.yml @@ -9,5 +9,9 @@ project: execute: freeze: auto - env: - PYTHONPATH: "." + +editor: + render-on-save: false + +citation: true +license: CC-BY-NC-SA diff --git a/book/quarto/config/shared/pdf/build-production-common.yml b/book/quarto/config/shared/pdf/build-production-common.yml index 1a0abe1843..b4723014d1 100644 --- a/book/quarto/config/shared/pdf/build-production-common.yml +++ b/book/quarto/config/shared/pdf/build-production-common.yml @@ -12,5 +12,6 @@ project: execute: freeze: false - env: - PYTHONPATH: "." + +citation: true +license: CC-BY-NC-SA diff --git a/book/quarto/contents/vol1/backmatter/appendix_algorithm.qmd b/book/quarto/contents/vol1/backmatter/appendix_algorithm.qmd index 29495795fb..a5541d2259 100644 --- a/book/quarto/contents/vol1/backmatter/appendix_algorithm.qmd +++ b/book/quarto/contents/vol1/backmatter/appendix_algorithm.qmd @@ -142,7 +142,7 @@ Or in einsum notation: `ik,kj->ij`. This notation extends naturally to the multi ### Memory Layouts and Performance {#sec-algorithm-foundations-memory-layouts-performance-44d0} -Data layout in memory (row-major vs. column-major) directly affects cache efficiency. When iterating over a matrix, accessing contiguous memory locations is dramatically faster than strided access. The difference can be 10$\times$ to 100$\times$ in effective bandwidth. +Data layout in memory (row-major vs. column-major) directly affects cache efficiency. When iterating over a matrix, accessing contiguous memory locations is dramatically faster than strided access. The difference can be 10 $\times$ to 100 $\times$ in effective bandwidth. A common optimization pattern: transpose tensors once before repeated operations to ensure contiguous access in the hot loop. The one-time transpose cost is amortized across many subsequent operations. @@ -165,8 +165,8 @@ The arithmetic intensity of GEMM scales linearly with matrix dimension. For squa This explains several important phenomena: - **Larger batches improve efficiency**: Batching increases the effective matrix dimensions, pushing workloads toward the compute-bound region of the roofline. -- **Power-of-two dimensions help**: Hardware tensor cores are optimized for specific tile sizes (typically 16$\times$16 or 32$\times$32). Dimensions that align with these sizes avoid padding overhead. -- **Small matrices are inefficient**: A `{python} n_small_str`$\times$`{python} n_small_str` GEMM has intensity `{python} n_small_str`/3 ≈ `{python} small_intensity_str` FLOP/byte, well below the ridge point (`{python} a100_ridge_str`), achieving only ~`{python} small_efficiency_pct_str`% of peak throughput. +- **Power-of-two dimensions help**: Hardware tensor cores are optimized for specific tile sizes (typically 16 $\times$ 16 or 32 $\times$ 32). Dimensions that align with these sizes avoid padding overhead. +- **Small matrices are inefficient**: A `{python} n_small_str` $\times$ `{python} n_small_str` GEMM has intensity `{python} n_small_str`/3 ≈ `{python} small_intensity_str` FLOP/byte, well below the ridge point (`{python} a100_ridge_str`), achieving only ~`{python} small_efficiency_pct_str`% of peak throughput. ### Sparse Matrix Formats {#sec-algorithm-foundations-sparse-matrix-formats-0bd0} @@ -184,7 +184,7 @@ To see the tradeoff concretely, consider a vocabulary embedding matrix of size ` * **Dense (FP32)**: `{python} total_elements_str` $\times$ 10⁹ $\times$ `{python} bytes_fp32_str` bytes = **`{python} dense_gb_str` GB**. * **Sparse (`{python} sparsity_pct_str`% density)**: Storing only non-zeros requires roughly `{python} nonzeros_str` $\times$ 10⁶ $\times$ (`{python} bytes_fp32_str` bytes value + `{python} bytes_int32_str` bytes index) ≈ **`{python} sparse_mb_str` MB**. -* *Result*: A `{python} reduction_factor_str`$\times$ reduction in memory footprint, fitting a model that would otherwise OOM (Out of Memory). +* *Result*: A `{python} reduction_factor_str` $\times$ reduction in memory footprint, fitting a model that would otherwise OOM (Out of Memory). Linear algebra tells us *what* to compute; the next question is *how* to express those computations in code. Tensor programming primitives—shapes, strides, and broadcasting—bridge the gap between mathematical notation and the array operations that actually execute on hardware. @@ -255,7 +255,7 @@ Modern frameworks use **reverse-mode automatic differentiation**, which computes Backpropagation[^fn-backprop] implements the chain rule efficiently through two passes: forward to compute outputs, backward to compute gradients. @fig-backprop-graph illustrates this process for a simple two-layer network, with the forward pass (black arrows) computing outputs and the backward pass (red dashed arrows) propagating gradients. -[^fn-backprop]: **Backpropagation**: Short for "backward propagation of errors." The algorithm was independently discovered multiple times—by Linnainmaa (1970) for automatic differentiation and by Rumelhart, Hinton, and Williams (1986) for neural network training. Its key insight is that computing gradients for *all* parameters requires only one backward pass through the graph, making training cost roughly 2–3$\times$ inference rather than $N\times$ (once per parameter). +[^fn-backprop]: **Backpropagation**: Short for "backward propagation of errors." The algorithm was independently discovered multiple times—by Linnainmaa (1970) for automatic differentiation and by Rumelhart, Hinton, and Williams (1986) for neural network training. Its key insight is that computing gradients for *all* parameters requires only one backward pass through the graph, making training cost roughly 2–3 $\times$ inference rather than $N\times$ (once per parameter). ::: {#fig-backprop-graph fig-env="figure" fig-pos="htb" fig-cap="**Backpropagation Computational Graph**: A two-layer network showing the forward pass (black arrows) and backward pass (red dashed arrows). Each node caches values during the forward pass that are reused during the backward pass." fig-alt="A computational graph with four nodes labeled x, h, y, and L connected left to right. Solid black arrows show the forward pass with weights W1 and W2. Dashed red arrows curve backward showing gradient flow with partial derivative notation."} ```{.tikz} @@ -344,7 +344,7 @@ $48 \times 2 \times 8 \times 1024 \times 1600 \times 2$ bytes $\approx$ 24 GB #### Activation Explosion {#sec-algorithm-foundations-activation-explosion-1da5} -While weights are fixed ($O(P)$), activations grow linearly with batch size and sequence length. For large language models, activations can be 10–50$\times$ larger than the weights themselves. This explosion is why techniques like gradient checkpointing[^fn-grad-checkpoint] [@chen2016training] and FlashAttention (tiling attention to reduce memory round-trips) are mandatory for large-scale training, not optional optimizations. +While weights are fixed ($O(P)$), activations grow linearly with batch size and sequence length. For large language models, activations can be 10–50 $\times$ larger than the weights themselves. This explosion is why techniques like gradient checkpointing[^fn-grad-checkpoint] [@chen2016training] and FlashAttention (tiling attention to reduce memory round-trips) are mandatory for large-scale training, not optional optimizations. [^fn-grad-checkpoint]: **Gradient Checkpointing**: Trades compute for memory. Instead of storing all activations, you store only a subset (checkpoints) and recompute the missing ones during the backward pass. This reduces memory usage from $O(N)$ to $O(\sqrt{N})$ at the cost of ~33% more compute. @@ -370,7 +370,7 @@ Without fusion, each operation in a chain like `MatMul → Add (bias) → ReLU` Fusion combines consecutive operations into a single kernel that reads the input once, applies all operations in registers or shared memory, and writes the final result once. For a sequence of $k$ elementwise operations on a tensor of size $N$ bytes, fusion reduces memory traffic from $2kN$ bytes (each op reads and writes) to $2N$ bytes (one read, one write)—a $k\times$ reduction. -FlashAttention is the most impactful fusion in modern ML: it fuses the entire attention computation (Q$\cdot$K$^T$ scaling, masking, softmax, dropout, and V multiplication) into a single kernel that operates on tiles in SRAM, reducing attention memory from $O(S^2)$ to $O(S)$ and achieving 2–4$\times$ wall-clock speedups by eliminating the large intermediate attention matrix that would otherwise be written to and read from HBM. +FlashAttention is the most impactful fusion in modern ML: it fuses the entire attention computation (Q$\cdot$K$^T$ scaling, masking, softmax, dropout, and V multiplication) into a single kernel that operates on tiles in SRAM, reducing attention memory from $O(S^2)$ to $O(S)$ and achieving 2–4 $\times$ wall-clock speedups by eliminating the large intermediate attention matrix that would otherwise be written to and read from HBM. Together, the linear algebra foundations, tensor programming mechanics, and training memory model covered in this appendix form the algorithmic substrate on which all ML systems are built. Before moving on, several common misconceptions deserve attention because they trip up even experienced practitioners. @@ -395,7 +395,7 @@ A matrix multiplication can run at 80% of peak throughput or 5% of peak dependin - GEMM arithmetic intensity scales as $n/3$ for square $n \times n$ matrices: small matrices are memory-bound and waste most of the hardware's compute capability. Increasing batch size or aligning dimensions to hardware tile sizes are the primary levers for improving throughput. - Sparse storage formats only reduce memory when sparsity exceeds roughly 90–95%, because index metadata consumes space proportional to the number of non-zeros. - Tensor shapes, strides, and broadcasting are the source of many subtle bugs: a non-contiguous tensor can silently degrade kernel performance by orders of magnitude, and an incorrect broadcast can produce a result of the wrong shape without raising an error. -- Training memory is dominated by four components—weights, gradients, optimizer states, and activations—and the last of these grows with batch size and sequence length, often exceeding the model's weight footprint by 10–50$\times$. +- Training memory is dominated by four components—weights, gradients, optimizer states, and activations—and the last of these grows with batch size and sequence length, often exceeding the model's weight footprint by 10–50 $\times$. - Backpropagation's efficiency comes from computing all parameter gradients in a single backward pass, but it requires caching forward-pass activations, creating a fundamental memory–compute tradeoff that gradient checkpointing partially resolves. - Computational graph representations enable compiler optimizations like operator fusion, which eliminates memory round-trips between consecutive operations and can dramatically improve throughput for memory-bound workloads. diff --git a/book/quarto/contents/vol1/backmatter/appendix_assumptions.qmd b/book/quarto/contents/vol1/backmatter/appendix_assumptions.qmd index ea9964f188..a354351f88 100644 --- a/book/quarto/contents/vol1/backmatter/appendix_assumptions.qmd +++ b/book/quarto/contents/vol1/backmatter/appendix_assumptions.qmd @@ -483,7 +483,7 @@ Energy-per-operation and energy-per-access constants come from semiconductor pro | **ENERGY_MOBILENET_INF_MJ** | `{python} ENERGY_MOBILENET_INF_MJ_val` | `{python} ENERGY_MOBILENET_INF_MJ_unit` | | **NETWORK_5G_ENERGY_PER_MB_MJ** | `{python} NETWORK_5G_ENERGY_PER_MB_MJ_val` | `{python} NETWORK_5G_ENERGY_PER_MB_MJ_unit` | -: **Energy per Operation and Access**: These constants quantify the energy hierarchy from register file through DRAM and across precision formats. The 200$\times$ gap between a register read and a DRAM access explains why data reuse (tiling, fusion) dominates ML kernel optimization. {#tbl-assumptions-energy} +: **Energy per Operation and Access**: These constants quantify the energy hierarchy from register file through DRAM and across precision formats. The 200 $\times$ gap between a register read and a DRAM access explains why data reuse (tiling, fusion) dominates ML kernel optimization. {#tbl-assumptions-energy} ::: @@ -597,11 +597,11 @@ With all constants, units, and scale references in place, it is worth pausing to **Fallacy:** *Peak FLOPS predict real-world training throughput.* -Datasheet FLOPS are measured under idealized conditions—perfectly aligned matrix dimensions, 100% occupancy, zero memory stalls. Real training workloads typically achieve 30–50% of peak (measured as Model FLOPS Utilization, or MFU). Using peak FLOPS to estimate training time without an MFU discount produces estimates that are 2–3$\times$ too optimistic, leading to missed deadlines and budget overruns. +Datasheet FLOPS are measured under idealized conditions—perfectly aligned matrix dimensions, 100% occupancy, zero memory stalls. Real training workloads typically achieve 30–50% of peak (measured as Model FLOPS Utilization, or MFU). Using peak FLOPS to estimate training time without an MFU discount produces estimates that are 2–3 $\times$ too optimistic, leading to missed deadlines and budget overruns. **Pitfall:** *Using FP32 FLOPS when the workload runs in BF16 or FP8.* -Modern GPUs have separate datapaths for different precisions, and the peak throughput varies dramatically: the H100 delivers 990 TFLOPS in FP16 tensor operations but only 60 TFLOPS in FP32 non-tensor operations—a 16$\times$ difference. Quoting the wrong precision's peak when computing utilization or estimating training time produces meaningless results. Always match the constant to the precision your workload actually uses. +Modern GPUs have separate datapaths for different precisions, and the peak throughput varies dramatically: the H100 delivers 990 TFLOPS in FP16 tensor operations but only 60 TFLOPS in FP32 non-tensor operations—a 16 $\times$ difference. Quoting the wrong precision's peak when computing utilization or estimating training time produces meaningless results. Always match the constant to the precision your workload actually uses. **Fallacy:** *Hardware constants are stable enough to hardcode.* @@ -617,7 +617,7 @@ Thermal Design Power (TDP) is the maximum sustained power draw the cooling syste - Every quantitative example in this book traces back to a specific constant in `mlsys/constants.py`. This appendix exposes all of those constants so you can audit, verify, or update the numbers that underpin the book's reasoning. - Hardware specs (peak FLOPS, memory bandwidth, TDP) set ceilings, not guarantees. Real utilization is typically 30–50% of peak for training workloads; using peak values without discounting produces dangerously optimistic estimates. -- The *ratios* between constants are often more stable and informative than the absolute values. The ridge point (FLOPS / bandwidth), the memory-per-parameter cost (16 bytes for mixed-precision Adam), and the energy hierarchy (200$\times$ between register and DRAM access) persist across hardware generations. +- The *ratios* between constants are often more stable and informative than the absolute values. The ridge point (FLOPS / bandwidth), the memory-per-parameter cost (16 bytes for mixed-precision Adam), and the energy hierarchy (200 $\times$ between register and DRAM access) persist across hardware generations. - A single source of truth for constants eliminates the most common source of inconsistency in quantitative textbooks: the same number quoted differently in different chapters. ::: diff --git a/book/quarto/contents/vol1/backmatter/appendix_dam.qmd b/book/quarto/contents/vol1/backmatter/appendix_dam.qmd index 634f0ba85c..ef94071ccb 100644 --- a/book/quarto/contents/vol1/backmatter/appendix_dam.qmd +++ b/book/quarto/contents/vol1/backmatter/appendix_dam.qmd @@ -441,7 +441,7 @@ The fix targets the **Data/Algorithm boundary**: increasing the batch size trans ##### Exercise 3: *Scaling Law vs. Information Roofline* {.unnumbered} -Your team has been training a sentiment analysis model. After scaling from `{python} ex3_params_start_str` to `{python} ex3_params_end_str` parameters (`{python} ex3_scale_factor`$\times$ increase), validation loss improved from `{python} ex3_loss_start` to `{python} ex3_loss_end` (`{python} ex3_imp_str`% improvement). Chinchilla scaling would predict a ~`{python} ex3_chin_pred_str`% improvement for this compute increase. What does this discrepancy suggest? Which D·A·M axis should you investigate first, and why? +Your team has been training a sentiment analysis model. After scaling from `{python} ex3_params_start_str` to `{python} ex3_params_end_str` parameters (`{python} ex3_scale_factor` $\times$ increase), validation loss improved from `{python} ex3_loss_start` to `{python} ex3_loss_end` (`{python} ex3_imp_str`% improvement). Chinchilla scaling would predict a ~`{python} ex3_chin_pred_str`% improvement for this compute increase. What does this discrepancy suggest? Which D·A·M axis should you investigate first, and why? *Answer*: The discrepancy suggests you have hit the **Information Roofline**—the ceiling imposed by data quality rather than model capacity. Scaling laws predict the *slope* of improvement assuming sufficient information in the data. When actual improvement (`{python} ex3_imp_str`%) falls well short of predicted improvement (`{python} ex3_chin_pred_str`%), the model has likely extracted most learnable signal from the training distribution. @@ -457,6 +457,6 @@ A colleague proposes upgrading from `{python} ex4_gpu_old_str` GPUs to `{python} 2. **"What percentage of each training step is spent waiting for data?"** → *Data*. Run `torch.profiler` or check if `DataLoader` time exceeds 10% of step time. If data loading dominates, the fix is pipeline optimization (binary formats, more workers, prefetching), not new GPUs. -3. **"Can the batch size scale with 2$\times$ more GPUs without degrading convergence?"** → *Algorithm*. Doubling GPUs typically requires doubling batch size to maintain efficiency. If the model already uses the maximum stable batch size, or if larger batches hurt convergence, additional GPUs provide diminishing returns. Check the scaling efficiency: if 4 GPUs achieve only 3$\times$ speedup over 1 GPU, the communication overhead suggests 8 GPUs might achieve only 4–5$\times$ speedup. +3. **"Can the batch size scale with 2 $\times$ more GPUs without degrading convergence?"** → *Algorithm*. Doubling GPUs typically requires doubling batch size to maintain efficiency. If the model already uses the maximum stable batch size, or if larger batches hurt convergence, additional GPUs provide diminishing returns. Check the scaling efficiency: if 4 GPUs achieve only 3 $\times$ speedup over 1 GPU, the communication overhead suggests 8 GPUs might achieve only 4–5 $\times$ speedup. Only if all three answers are favorable—high utilization, minimal data wait, and batch size headroom—does the hardware upgrade make sense. diff --git a/book/quarto/contents/vol1/backmatter/appendix_data.qmd b/book/quarto/contents/vol1/backmatter/appendix_data.qmd index 9a752d3b3a..92ce8320a4 100644 --- a/book/quarto/contents/vol1/backmatter/appendix_data.qmd +++ b/book/quarto/contents/vol1/backmatter/appendix_data.qmd @@ -171,7 +171,7 @@ Even after data arrives at the machine, we face one final hurdle: the serializat | **Protobuf** | `{python} proto_speed_str` MB/s | `{python} proto_cycles_str` cycles | RPC / Messages | | **Parquet / Arrow** | **`{python} parquet_speed_str` MB/s** | **`{python} parquet_cycles_str` cycles** | **High-Scale Training** | -: **Serialization Overhead.** Zero-copy formats like Arrow are 10–100$\times$ faster than row-based text formats because they align directly with internal memory structures. {#tbl-serialization-cost} +: **Serialization Overhead.** Zero-copy formats like Arrow are 10–100 $\times$ faster than row-based text formats because they align directly with internal memory structures. {#tbl-serialization-cost} ### Row vs. Columnar Formats {#sec-data-foundations-row-vs-columnar-formats-bca2} @@ -403,7 +403,7 @@ Transfer time is only the beginning. Data gravity encompasses not just transfer **Pitfall:** *Ignoring data serialization cost.* -Engineers meticulously optimize accelerator kernels while leaving data loading as JSON or CSV. As @tbl-serialization-cost shows, text-based formats can be 10–100$\times$ slower to decode than columnar binary formats, making the CPU—not the accelerator—the bottleneck (@sec-data-foundations-data-engineering-foundations-0c20). +Engineers meticulously optimize accelerator kernels while leaving data loading as JSON or CSV. As @tbl-serialization-cost shows, text-based formats can be 10–100 $\times$ slower to decode than columnar binary formats, making the CPU—not the accelerator—the bottleneck (@sec-data-foundations-data-engineering-foundations-0c20). **Fallacy:** *Average latency is a good summary of system performance.* @@ -420,7 +420,7 @@ These pitfalls share a common root: treating data as a solved problem rather tha ::: {.callout-takeaways title="Data as a Physical Constraint"} - Data has *physical inertia*: transfer time scales linearly with volume and inversely with bandwidth, making petabyte-scale datasets effectively immovable. Design pipelines around data locality rather than data movement. -- Serialization format is a first-order performance decision. Columnar binary formats (Parquet, Arrow) can be 10–100$\times$ faster to decode than text formats (CSV, JSON), directly determining whether accelerators starve or stay fed. +- Serialization format is a first-order performance decision. Columnar binary formats (Parquet, Arrow) can be 10–100 $\times$ faster to decode than text formats (CSV, JSON), directly determining whether accelerators starve or stay fed. - The three algebraic primitives—selection, projection, and join—have radically different I/O costs. Joins in particular can amplify network traffic by orders of magnitude; choosing between shuffle and broadcast joins depends on relative table sizes. - Average metrics lie. Long-tailed distributions mean that the P99 latency becomes the typical user experience at scale, and monitoring only the mean creates dangerous blind spots. - Drift detection requires comparing full distributions, not summary statistics. KL divergence and its symmetric derivative PSI provide principled measures of distribution shift that catch problems before accuracy metrics react. diff --git a/book/quarto/contents/vol1/backmatter/appendix_machine.qmd b/book/quarto/contents/vol1/backmatter/appendix_machine.qmd index 4799884c73..d3a40683a3 100644 --- a/book/quarto/contents/vol1/backmatter/appendix_machine.qmd +++ b/book/quarto/contents/vol1/backmatter/appendix_machine.qmd @@ -111,7 +111,7 @@ Just as Jeff Dean's "Latency Numbers Every Programmer Should Know"[^fn-jeff-dean ::: {.callout-takeaways title="Three Numbers That Matter Most"} If you memorize nothing else from this section, memorize these: -1. **~600$\times$ energy ratio**: DRAM access costs ~`{python} dram_vs_compute`$\times$ more energy than an FP16 multiply-add. This is why arithmetic intensity is everything. +1. **~600 $\times$ energy ratio**: DRAM access costs ~`{python} dram_vs_compute` $\times$ more energy than an FP16 multiply-add. This is why arithmetic intensity is everything. 2. **16 bytes/parameter for training**: Model weights (2B FP16) + master weights (4B FP32) + optimizer states (8B Adam). A 7B model needs 112 GB just to start training. @@ -141,46 +141,46 @@ These relationships are governed by physics or arithmetic—they will still be t @tbl-energy-ratios-ref quantifies the energy cost of data movement versus computation—the fundamental reason why arithmetic intensity dominates ML performance optimization. -| Relationship | Ratio | Why It's Stable | -|:-----------------------------|------------------------------------:|:--------------------------------------| -| DRAM access vs. FP16 compute | ~`{python} dram_vs_compute`$\times$ | Wire capacitance scales with distance | -| FP32 vs. INT8 energy | ~`{python} fp32_vs_int8`$\times$ | Bit width determines switching energy | -| FP32 vs. FP16 energy | ~`{python} fp32_vs_fp16`$\times$ | Halving bits roughly halves energy | -| L1 SRAM vs. register | ~`{python} l1_vs_reg`$\times$ | Distance to ALU | +| Relationship | Ratio | Why It's Stable | +|:-----------------------------|-------------------------------------:|:--------------------------------------| +| DRAM access vs. FP16 compute | ~`{python} dram_vs_compute` $\times$ | Wire capacitance scales with distance | +| FP32 vs. INT8 energy | ~`{python} fp32_vs_int8` $\times$ | Bit width determines switching energy | +| FP32 vs. FP16 energy | ~`{python} fp32_vs_fp16` $\times$ | Halving bits roughly halves energy | +| L1 SRAM vs. register | ~`{python} l1_vs_reg` $\times$ | Distance to ALU | -: **The Energy Wall.** Moving data costs ~580$\times$ more energy than computing on it. This ratio is physics, not engineering.[^fn-horowitz-energy] {#tbl-energy-ratios-ref} +: **The Energy Wall.** Moving data costs ~580 $\times$ more energy than computing on it. This ratio is physics, not engineering.[^fn-horowitz-energy] {#tbl-energy-ratios-ref} [^fn-horowitz-energy]: Energy numbers from Horowitz's classic "Computing's Energy Problem" (ISSCC 2014, 45nm process). While absolute values scale with process node, the *ratios* between memory access and compute remain remarkably stable because wire capacitance (distance) dominates. #### Memory Hierarchy {.unnumbered} -@tbl-memory-ratios-ref shows how each level of the memory hierarchy costs roughly 10–100$\times$ more latency than the one above it. +@tbl-memory-ratios-ref shows how each level of the memory hierarchy costs roughly 10–100 $\times$ more latency than the one above it. -| Relationship | Ratio | Why It Persists | -|:-------------------------------|--------------------------------------------:|:----------------------------------| -| GPU memory (HBM) vs. L1 cache | ~`{python} hbm_vs_l1`$\times$ slower | On-chip vs. off-chip | -| SSD vs. L1 cache | ~`{python} ssd_vs_l1`$\times$ slower | Electrical vs. mechanical/flash | -| Network vs. local memory | ~`{python} network_vs_local`$\times$ slower | Speed of light + switching | -| GPU memory BW vs. CPU↔GPU link | ~`{python} gpu_bw_vs_pcie`$\times$ faster | Architectural investment priority | +| Relationship | Ratio | Why It Persists | +|:-------------------------------|---------------------------------------------:|:----------------------------------| +| GPU memory (HBM) vs. L1 cache | ~`{python} hbm_vs_l1` $\times$ slower | On-chip vs. off-chip | +| SSD vs. L1 cache | ~`{python} ssd_vs_l1` $\times$ slower | Electrical vs. mechanical/flash | +| Network vs. local memory | ~`{python} network_vs_local` $\times$ slower | Speed of light + switching | +| GPU memory BW vs. CPU↔GPU link | ~`{python} gpu_bw_vs_pcie` $\times$ faster | Architectural investment priority | -: **The Latency Hierarchy.** Each level costs roughly 10–100$\times$ more than the one above it. {#tbl-memory-ratios-ref} +: **The Latency Hierarchy.** Each level costs roughly 10–100 $\times$ more than the one above it. {#tbl-memory-ratios-ref} #### Scaling Laws {.unnumbered} @tbl-scaling-rules-ref collects the arithmetic relationships that govern memory and compute requirements for training and inference. -| Rule | Formula | Example | -|:------------------------------|:---------------------------------------|:---------------------------------------| -| Inference memory (FP16) | 2 bytes $\times$ parameters | 7B params → 14 GB | -| Inference memory (INT8) | 1 byte $\times$ parameters | 7B params → 7 GB | -| Training memory (Adam) | 16 bytes $\times$ parameters | 7B params → 112 GB | -| Inference FLOPs (transformer) | ~2 $\times$ parameters per token | 7B model → ~14 GFLOPs/token | -| Training FLOPs | ~6 $\times$ parameters $\times$ tokens | 7B on 1T tokens → 4$\times$10²² FLOPs | -| Datacenter vs. edge compute | ~`{python} dc_mobile_ratio`$\times$ | Compute per watt $\times$ power budget | +| Rule | Formula | Example | +|:------------------------------|:---------------------------------------|:----------------------------------------| +| Inference memory (FP16) | 2 bytes $\times$ parameters | 7B params → 14 GB | +| Inference memory (INT8) | 1 byte $\times$ parameters | 7B params → 7 GB | +| Training memory (Adam) | 16 bytes $\times$ parameters | 7B params → 112 GB | +| Inference FLOPs (transformer) | ~2 $\times$ parameters per token | 7B model → ~14 GFLOPs/token | +| Training FLOPs | ~6 $\times$ parameters $\times$ tokens | 7B on 1T tokens → 4 $\times$ 10²² FLOPs | +| Datacenter vs. edge compute | ~`{python} dc_mobile_ratio` $\times$ | Compute per watt $\times$ power budget | : **Scaling Rules.** These are arithmetic, not hardware-specific. Training memory includes FP16 weights (2B), FP32 master weights (4B), and Adam optimizer states (8B for momentum + variance).[^fn-training-memory] {#tbl-scaling-rules-ref} -[^fn-training-memory]: The 16 bytes/parameter rule assumes mixed-precision training with Adam. ZeRO optimization can reduce per-GPU memory by sharding optimizer states across GPUs, but the total memory across all GPUs remains ~16$\times$ parameters. +[^fn-training-memory]: The 16 bytes/parameter rule assumes mixed-precision training with Adam. ZeRO optimization can reduce per-GPU memory by sharding optimizer states across GPUs, but the total memory across all GPUs remains ~16 $\times$ parameters. ### Latency Budgets: The Non-Negotiables { .unnumbered} @@ -198,7 +198,7 @@ These budgets are set by physics (safety) or psychology (human perception)—not ### Current Hardware Reference (c. 2024) {.unnumbered} -These numbers reflect the current generation. Use them for back-of-envelope calculations, but expect them to improve ~2$\times$ every 2–3 years. +These numbers reflect the current generation. Use them for back-of-envelope calculations, but expect them to improve ~2 $\times$ every 2–3 years. #### Memory Latency and Bandwidth {.unnumbered} @@ -227,7 +227,7 @@ These numbers reflect the current generation. Use them for back-of-envelope calc | Datacenter GPU (A100) | `{python} flops_a100_fp16` TFLOPS | 624 TOPS | 400W | | Mobile NPU | — | `{python} flops_mobile_int8` TOPS | 3–5W | -: **Compute Reference (c. 2024).** Datacenter is ~28$\times$ more powerful than mobile—this ratio persists across generations. {#tbl-compute-current-ref} +: **Compute Reference (c. 2024).** Datacenter is ~28 $\times$ more powerful than mobile—this ratio persists across generations. {#tbl-compute-current-ref} #### Roofline Ridge Points {.unnumbered} @@ -395,9 +395,9 @@ Consider an NVIDIA A100 GPU with FP16 Tensor Core performance of `{python} a100_ Now compare two common operations: -**GEMM (Matrix Multiplication)**: For two `{python} n_gemm`$\times$`{python} n_gemm` matrices, arithmetic intensity is approximately `{python} gemm_intensity` FLOP/byte. Since `{python} gemm_intensity` > `{python} ridge_point`, this operation is compute-bound. You are using the hardware efficiently. +**GEMM (Matrix Multiplication)**: For two `{python} n_gemm` $\times$ `{python} n_gemm` matrices, arithmetic intensity is approximately `{python} gemm_intensity` FLOP/byte. Since `{python} gemm_intensity` > `{python} ridge_point`, this operation is compute-bound. You are using the hardware efficiently. -**ReLU (Element-wise)**: For a `{python} n_gemm`$\times$`{python} n_gemm` tensor, intensity is approximately `{python} relu_intensity` op/byte. Since `{python} relu_intensity` ≪ `{python} ridge_point`, this operation is severely memory-bound, achieving only about `{python} relu_util_str`% of peak TFLOP/s. The hardware is mostly waiting for data. +**ReLU (Element-wise)**: For a `{python} n_gemm` $\times$ `{python} n_gemm` tensor, intensity is approximately `{python} relu_intensity` op/byte. Since `{python} relu_intensity` ≪ `{python} ridge_point`, this operation is severely memory-bound, achieving only about `{python} relu_util_str`% of peak TFLOP/s. The hardware is mostly waiting for data. This explains why modern frameworks fuse operations: combining ReLU with the preceding MatMul avoids writing intermediate results to memory, effectively increasing arithmetic intensity. @@ -502,16 +502,16 @@ amdahl_inf_str = fmt(amdahl_inf_value, precision=0, commas=False) ``` * With $n=1$, speedup is 1. -* With n=`{python} n_8_str`, speedup is 1/(`{python} s_str` + `{python} p_str`/`{python} n_8_str`) ≈ `{python} amdahl_8_str`$\times$. -* With n=infinity, speedup is capped at 1/`{python} s_str` = `{python} amdahl_inf_str`$\times$. +* With n=`{python} n_8_str`, speedup is 1/(`{python} s_str` + `{python} p_str`/`{python} n_8_str`) ≈ `{python} amdahl_8_str` $\times$. +* With n=infinity, speedup is capped at 1/`{python} s_str` = `{python} amdahl_inf_str` $\times$. -No matter how many accelerators you buy, you cannot make this fixed workload run faster than `{python} amdahl_inf_str`$\times$. +No matter how many accelerators you buy, you cannot make this fixed workload run faster than `{python} amdahl_inf_str` $\times$. #### Weak Scaling (Gustafson's Law) {#sec-machine-foundations-weak-scaling-gustafsons-law-eb7e} **Weak scaling** answers the question: *If I add more processors, how much larger of a problem can I solve in the same amount of time?* -This is the reality of Large Language Models. We don't use 1,000 GPUs to train GPT-4 on a laptop-sized dataset in milliseconds; we use them to train on a dataset 1,000$\times$ larger in reasonable time. +This is the reality of Large Language Models. We don't use 1,000 GPUs to train GPT-4 on a laptop-sized dataset in milliseconds; we use them to train on a dataset 1,000 $\times$ larger in reasonable time. Gustafson's Law [@gustafson1988reevaluating] models this "scaled speedup":[^fn-gustafson] @@ -583,8 +583,8 @@ n_1000_minus_1_str = str(n_1000_value - 1) ``` * With $n=1$, speedup is 1. -* With n=`{python} n_8_g_str`, Scaled Speedup is `{python} n_8_g_str` - `{python} s_g_str` $\times$ (`{python} n_8_g_minus_1_str`) = `{python} n_8_g_str` - `{python} gustafson_8_serial` = `{python} gustafson_8_str`$\times$. -* With n=`{python} n_1000_str`, Scaled Speedup is `{python} n_1000_str` - `{python} s_g_str` $\times$ (`{python} n_1000_minus_1_str`) ≈ `{python} gustafson_1000_str`$\times$. +* With n=`{python} n_8_g_str`, Scaled Speedup is `{python} n_8_g_str` - `{python} s_g_str` $\times$ (`{python} n_8_g_minus_1_str`) = `{python} n_8_g_str` - `{python} gustafson_8_serial` = `{python} gustafson_8_str` $\times$. +* With n=`{python} n_1000_str`, Scaled Speedup is `{python} n_1000_str` - `{python} s_g_str` $\times$ (`{python} n_1000_minus_1_str`) ≈ `{python} gustafson_1000_str` $\times$. In weak scaling, efficiency remains high because the useful work (training the model) scales up to dwarf the fixed overheads. @@ -791,7 +791,7 @@ While latency tells us how long we wait for the *first* byte, bandwidth tells us Computer systems use a hierarchy because no single technology provides both high capacity and low latency. Examine the pyramid in @fig-memory-hierarchy to see how each level balances this tradeoff: every technique that keeps data higher in the pyramid (registers/cache) directly improves performance. -::: {#fig-memory-hierarchy fig-env="figure" fig-pos="htb" fig-cap="**The Memory Hierarchy**: Performance depends on data proximity. Accessing HBM is ~100$\times$ slower than registers; accessing SSD is ~100,000$\times$ slower." fig-alt="Pyramid showing Registers at top, followed by Cache, HBM/DRAM, and Storage at bottom."} +::: {#fig-memory-hierarchy fig-env="figure" fig-pos="htb" fig-cap="**The Memory Hierarchy**: Performance depends on data proximity. Accessing HBM is ~100 $\times$ slower than registers; accessing SSD is ~100,000 $\times$ slower." fig-alt="Pyramid showing Registers at top, followed by Cache, HBM/DRAM, and Storage at bottom."} ```{.tikz} \begin{tikzpicture}[line cap=round, line join=round, font=\usefont{T1}{phv}{m}{n}\small, scale=1.0] \tikzset{ @@ -814,7 +814,7 @@ Computer systems use a hierarchy because no single technology provides both high The hierarchy's energy costs reveal why data movement dominates modern system design. ::: {.callout-notebook title="The High Cost of Data Movement"} -Fetching a 32-bit value from DRAM costs roughly **`{python} energy_ratio_str`$\times$ more energy** than performing a floating-point operation on it (e.g., ~`{python} dram_pj` pJ vs ~`{python} flop_pj` pJ). This "Energy Wall" means that maximizing **arithmetic intensity** (doing many ops per loaded byte) is the only way to be energy efficient. +Fetching a 32-bit value from DRAM costs roughly **`{python} energy_ratio_str` $\times$ more energy** than performing a floating-point operation on it (e.g., ~`{python} dram_pj` pJ vs ~`{python} flop_pj` pJ). This "Energy Wall" means that maximizing **arithmetic intensity** (doing many ops per loaded byte) is the only way to be energy efficient. ::: ### Bandwidth vs. Latency {#sec-machine-foundations-bandwidth-vs-latency-5320} @@ -1017,7 +1017,7 @@ This assumes perfect strong scaling (Amdahl's Law). In practice, communication o A related misconception concerns numerical precision. ::: {.callout-warning title="Fallacy: Higher precision (FP32) is always better."} -For deep learning, FP32 often hurts performance without improving convergence. It consumes 2$\times$ memory bandwidth and energy compared to BF16. Since neural networks are resilient to noise, the extra mantissa bits in FP32 are often modeling random variance rather than signal. +For deep learning, FP32 often hurts performance without improving convergence. It consumes 2 $\times$ memory bandwidth and energy compared to BF16. Since neural networks are resilient to noise, the extra mantissa bits in FP32 are often modeling random variance rather than signal. ::: @@ -1025,12 +1025,12 @@ With the common misconceptions addressed, use the reference numbers and models i :::: {.callout-takeaways title="Numbers Every Engineer Should Know"} -- **Energy dominates**: Moving data costs ~600$\times$ more energy than computing on it. Arithmetic intensity—the ratio of compute to data movement—is the single most important metric for ML workload performance. +- **Energy dominates**: Moving data costs ~600 $\times$ more energy than computing on it. Arithmetic intensity—the ratio of compute to data movement—is the single most important metric for ML workload performance. - **The Roofline Model** reveals whether a workload is compute-bound or memory-bound. Most inference workloads fall below the ridge point and are memory-bound; batch size is the primary lever to shift toward compute-bound operation. - **Amdahl's Law** caps strong-scaling speedup at $1/s$ (where $s$ is the serial fraction). **Gustafson's Law** shows that scaling the problem alongside hardware yields near-linear throughput gains—the paradigm that makes large-scale training feasible. - **Little's Law** ($L = \lambda W$) directly sizes inference infrastructure: concurrency, memory, and maximum throughput are all linked by this simple identity. - **Memory hierarchy** spans five orders of magnitude in latency (register at ~0.3 ns to SSD at ~100,000 ns). Keeping data close to compute is not an optimization—it is the optimization. -- **Numerical precision** is a systems lever, not just a modeling choice. BF16 matches FP32's dynamic range at half the memory cost; INT8 quantization can deliver 2–4$\times$ inference speedup with careful calibration. +- **Numerical precision** is a systems lever, not just a modeling choice. BF16 matches FP32's dynamic range at half the memory cost; INT8 quantization can deliver 2–4 $\times$ inference speedup with careful calibration. - **Physics is non-negotiable**: speed of light sets latency floors, energy ratios set efficiency ceilings, and no amount of software optimization can violate these constraints. :::: diff --git a/book/quarto/contents/vol1/backmatter/glossary/glossary.qmd b/book/quarto/contents/vol1/backmatter/glossary/glossary.qmd index 72df4e522f..c4f7485c1a 100644 --- a/book/quarto/contents/vol1/backmatter/glossary/glossary.qmd +++ b/book/quarto/contents/vol1/backmatter/glossary/glossary.qmd @@ -537,7 +537,7 @@ This glossary defines key terms used throughout this book. Terms are organized a : 32-bit floating-point numerical representation that provides standard precision for mathematical computations but requires more memory and computational resources than lower-precision formats. **fp32 to int8** -: A common quantization transformation that converts 32-bit floating point weights and activations to 8-bit integers, achieving roughly 4$\times$ memory reduction while maintaining acceptable accuracy for many models. +: A common quantization transformation that converts 32-bit floating point weights and activations to 8-bit integers, achieving roughly 4 $\times$ memory reduction while maintaining acceptable accuracy for many models. **framework decomposition** : The systematic breakdown of neural network frameworks into hardware-mappable components, enabling efficient distribution of operations across processing elements. @@ -666,7 +666,7 @@ This glossary defines key terms used throughout this book. Terms are organized a : The interface between software and hardware that defines the set of instructions a processor can execute, including data types and addressing modes. **int8** -: 8-bit integer numerical representation used in quantized neural networks, where model weights and activations are represented using 8-bit integers instead of 32-bit floating point, reducing memory usage by roughly 4$\times$ and accelerating inference on specialized hardware while attempting to maintain model accuracy. +: 8-bit integer numerical representation used in quantized neural networks, where model weights and activations are represented using 8-bit integers instead of 32-bit floating point, reducing memory usage by roughly 4 $\times$ and accelerating inference on specialized hardware while attempting to maintain model accuracy. **internet of things** : A network of physical objects embedded with sensors, software, and other technologies that connect and exchange data with other devices and systems over the internet. diff --git a/book/quarto/contents/vol1/backmatter/references.bib b/book/quarto/contents/vol1/backmatter/references.bib index 4682363484..f1b45692d3 100644 --- a/book/quarto/contents/vol1/backmatter/references.bib +++ b/book/quarto/contents/vol1/backmatter/references.bib @@ -3698,6 +3698,16 @@ url = {http://www.deeplearningbook.org}, } +@article{goodhart1984problems, + title = {Problems of Monetary Management: The {U.K.} Experience}, + author = {Goodhart, Charles A. E.}, + year = {1984}, + journal = {Monetary Theory and Practice}, + publisher = {Macmillan}, + pages = {91--121}, + note = {Originally presented in 1975; the principle now known as Goodhart's Law}, +} + @misc{google_bfloat16, title = {BFloat16: The secret to high performance on Cloud TPUs}, author = {Cloud, Google}, @@ -5299,6 +5309,20 @@ date = {2019-10-29}, } +@article{kendall1953stochastic, + title = { + Stochastic Processes Occurring in the Theory of Queues and Their Analysis by the Method of the + Imbedded {Markov} Chain + }, + author = {Kendall, David G.}, + year = {1953}, + journal = {The Annals of Mathematical Statistics}, + volume = {24}, + number = {3}, + pages = {338--354}, + doi = {10.1214/aoms/1177728975}, +} + @misc{keras, title = {Keras}, author = {Chollet, Fran\c{c}ois and others}, @@ -6320,6 +6344,17 @@ edition = {3rd}, } +@article{markowitz1952portfolio, + title = {Portfolio Selection}, + author = {Markowitz, Harry}, + year = {1952}, + journal = {The Journal of Finance}, + volume = {7}, + number = {1}, + pages = {77--91}, + doi = {10.2307/2975974}, +} + @misc{martin_fowler_canary, title = {Canary Release}, author = {Fowler, Martin}, @@ -9026,6 +9061,17 @@ date = {1969-08}, } +@article{strathern1997improving, + title = {`Improving Ratings': Audit in the {British} University System}, + author = {Strathern, Marilyn}, + year = {1997}, + journal = {European Review}, + volume = {5}, + number = {3}, + pages = {305--321}, + doi = {10.1002/(SICI)1234-981X(199707)5:3<305::AID-EURO184>3.0.CO;2-4}, +} + @article{strickland2019ibm, title = {IBM Watson, heal thyself: How IBM overpromised and underdelivered on AI health care}, author = {Strickland, Eliza}, @@ -9115,6 +9161,16 @@ date = {2019}, } +@inproceedings{sutskever2014sequence, + title = {Sequence to Sequence Learning with Neural Networks}, + author = {Sutskever, Ilya and Vinyals, Oriol and Le, Quoc V.}, + year = {2014}, + booktitle = {Advances in Neural Information Processing Systems}, + volume = {27}, + pages = {3104--3112}, + url = {https://arxiv.org/abs/1409.3215}, +} + @misc{sutton2019bitter, title = {The Bitter Lesson}, author = {Sutton, Richard S.}, diff --git a/book/quarto/contents/vol1/benchmarking/benchmarking.qmd b/book/quarto/contents/vol1/benchmarking/benchmarking.qmd index f1ff141e54..b1651b8255 100644 --- a/book/quarto/contents/vol1/benchmarking/benchmarking.qmd +++ b/book/quarto/contents/vol1/benchmarking/benchmarking.qmd @@ -19,7 +19,7 @@ engine: jupyter ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{65}{10}{25}{25}{35}{45}{10}{25} +\mlsysstack{75}{15}{25}{30}{35}{45}{10}{15} \end{marginfigure} _How do you systematically compare ML systems when hardware, models, data, and deployment conditions all interact?_ @@ -195,7 +195,7 @@ energy_slow_wh_str = fmt(energy_slow_wh_value, precision=5, commas=False) ## ML Benchmarking Framework {#sec-benchmarking-machine-learning-benchmarking-framework-70b8} -The optimization techniques from preceding chapters all claim improvements. Data selection strategies (@sec-data-selection) promise more efficient training. Model compression (@sec-model-compression) promises smaller, faster models. Hardware acceleration (@sec-hardware-acceleration) promises higher throughput. Yet *how* do we know these claims hold in production? A model quantized to INT8 may benchmark 2$\times$ faster on a synthetic workload but show no improvement under real traffic patterns with variable input sizes and concurrent requests. A pruned model may maintain accuracy on the test set but fail on edge cases the benchmark never covered. Benchmarking is the discipline of verifying that optimizations deliver their promised benefits under realistic conditions. +The optimization techniques from preceding chapters all claim improvements. Data selection strategies (@sec-data-selection) promise more efficient training. Model compression (@sec-model-compression) promises smaller, faster models. Hardware acceleration (@sec-hardware-acceleration) promises higher throughput. Yet *how* do we know these claims hold in production? A model quantized to INT8 may benchmark 2 $\times$ faster on a synthetic workload but show no improvement under real traffic patterns with variable input sizes and concurrent requests. A pruned model may maintain accuracy on the test set but fail on edge cases the benchmark never covered. Benchmarking is the discipline of verifying that optimizations deliver their promised benefits under realistic conditions. \index{System Benchmarking!definition} \index{Model Benchmarking!definition} @@ -229,7 +229,7 @@ Throughout this chapter, we validate the complete optimization pipeline using ** \index{Quantization!INT8 compression ratio} \index{Edge Accelerator!inference latency} -1. **Model Compression** (@sec-model-compression): INT8 quantization reduces MobileNet from `{python} mobilenet_v1_size_mb_str` MB to `{python} mobilenet_v1_int8_size_mb_str` MB (`{python} mobilenet_v1_compression_ratio_str`$\times$ compression) +1. **Model Compression** (@sec-model-compression): INT8 quantization reduces MobileNet from `{python} mobilenet_v1_size_mb_str` MB to `{python} mobilenet_v1_int8_size_mb_str` MB (`{python} mobilenet_v1_compression_ratio_str` $\times$ compression) 2. **Hardware Acceleration** (@sec-hardware-acceleration): EdgeTPU deployment achieves `{python} edgetpu_latency_ms_str` ms inference versus `{python} cpu_latency_ms_str` ms on CPU 3. **Benchmarking Validation** (this chapter): Verify the pipeline delivers in practice @@ -250,9 +250,9 @@ First, benchmarks are proxies, not truth. Every benchmark measures specific cond Second, Goodhart's Law applies everywhere.[^fn-goodharts-law]\index{Goodhart's Law!metric trap} "When a measure becomes a target, it ceases to be a good measure." Teams that optimize for benchmark rankings often produce systems that excel in evaluation but fail in production. Benchmark-specific optimizations frequently degrade characteristics—robustness, calibration, efficiency—that matter for deployment. \index{Benchmarking!end-to-end vs. component metrics} -Third, end-to-end beats component metrics. Vendors report component latency (5–10 ms for model inference), but production latency includes preprocessing, queuing, and postprocessing (50–100 ms total). A 3$\times$ inference speedup in isolation might yield only 1.3$\times$ end-to-end improvement, or worse if the optimization increases memory pressure. +Third, end-to-end beats component metrics. Vendors report component latency (5–10 ms for model inference), but production latency includes preprocessing, queuing, and postprocessing (50–100 ms total). A 3 $\times$ inference speedup in isolation might yield only 1.3 $\times$ end-to-end improvement, or worse if the optimization increases memory pressure. -[^fn-goodharts-law]: **Goodhart's Law**\index{Goodhart's Law!etymology}: Articulated by Charles Goodhart, a Bank of England economist, in a 1975 paper on monetary policy. Generalized by Marilyn Strathern (1997) as: "When a measure becomes a target, it ceases to be a good measure." Pervasive in ML: BLEU scores incentivize n-gram matching over fluency, ImageNet accuracy rewards architecture tricks over robustness, and benchmark leaderboards incentivize overfitting to test distributions. +[^fn-goodharts-law]: **Goodhart's Law**\index{Goodhart's Law!etymology}: Articulated by Charles Goodhart, a Bank of England economist, in a paper on monetary policy [@goodhart1984problems]. Generalized by Strathern [@strathern1997improving] as: "When a measure becomes a target, it ceases to be a good measure." Pervasive in ML: BLEU scores incentivize n-gram matching over fluency, ImageNet accuracy rewards architecture tricks over robustness, and benchmark leaderboards incentivize overfitting to test distributions. These principles reappear throughout this chapter and are examined in depth in @sec-benchmarking-fallacies-pitfalls-9781. @@ -295,6 +295,7 @@ DAWNBench [@coleman2017dawnbench] emerged as an early ML benchmark that pioneere \index{Patterson, David!MLPerf leadership} \index{MLCommons!benchmark organization} + [^fn-mlperf]: **MLPerf**\index{MLPerf!etymology}: Founded in 2018 by researchers from Google, NVIDIA, Intel, Harvard, Stanford, and UC Berkeley, led by David Patterson [@mattson2020mlperf]. The name combines "ML" with "Perf" (performance), echoing SPEC's benchmarking tradition. Now maintained by MLCommons, MLPerf provides suites for training (time-to-quality), inference (queries per second under latency constraints), and power (performance per watt). Its design principles---representative workloads, full-system measurement, and open submission---directly address the gaming problems that plagued earlier benchmarks. ### Energy Benchmarks {#sec-benchmarking-energy-benchmarks-709a} @@ -304,7 +305,7 @@ DAWNBench [@coleman2017dawnbench] emerged as an early ML benchmark that pioneere \index{Green500!HPC energy efficiency ranking} The multi-objective evaluation paradigm naturally extended to energy efficiency as computing diversified beyond mainframes with unlimited power budgets. Mobile devices demanded battery life optimization, while warehouse-scale systems faced energy costs rivaling hardware expenses. This shift established energy as a first-class metric alongside performance, spawning benchmarks like SPEC Power[^fn-spec-power] for servers, Green500[^fn-green500] for supercomputers, and ENERGY STAR[^fn-energy-star] for consumer systems. -[^fn-spec-power]: **SPEC Power**: Introduced in 2007 to address the growing importance of energy efficiency in server design, SPEC Power measures performance per watt across 11 load levels from idle (0%) through 100% in 10% increments. Results show that modern servers achieve 8–12 SPECpower_ssj2008 scores per watt, compared to 1–3 for systems from the mid-2000s, representing approximately 3–4$\times$ efficiency improvement. +[^fn-spec-power]: **SPEC Power**: Introduced in 2007 to address the growing importance of energy efficiency in server design, SPEC Power measures performance per watt across 11 load levels from idle (0%) through 100% in 10% increments. Results show that modern servers achieve 8–12 SPECpower_ssj2008 scores per watt, compared to 1–3 for systems from the mid-2000s, representing approximately 3–4 $\times$ efficiency improvement. [^fn-green500]: **Green500**: Started in 2007 as a counterpart to the Top500 supercomputer list, Green500 ranks systems by FLOPS per watt rather than raw performance. The most efficient systems achieve over 60 gigaFLOPS per watt compared to less than 1 gigaFLOPS/watt for early 2000s supercomputers, demonstrating improvements in computational efficiency. @@ -313,7 +314,7 @@ The multi-objective evaluation paradigm naturally extended to energy efficiency \index{MLPerf Power!ML energy measurement} Diverse workload patterns and system configurations continue to challenge power benchmarking across computing environments. MLPerf Power [@mlperf_power_website] addresses this with specialized methodologies for measuring the energy impact of machine learning workloads, reflecting energy efficiency's central role in AI system design. -Energy benchmarking extends beyond hardware power measurement to include algorithmic efficiency. Model compression techniques (pruning, quantization, knowledge distillation) often achieve greater energy savings than hardware improvements alone. MobileNet architectures achieve approximately `{python} mobilenet_flops_ratio_str`$\times$ fewer FLOPs versus ResNet, translating to proportional energy reduction on hardware that efficiently exploits the smaller model [@howard2017mobilenets]. These techniques, detailed in @sec-model-compression, establish that energy-aware benchmarking must evaluate algorithmic efficiency alongside hardware power consumption; the specific energy breakdown of INT8 versus FP32 is quantified in @sec-benchmarking-training-metrics-0f1a. As AI systems scale, this lesson becomes central to sustainable computing practices. +Energy benchmarking extends beyond hardware power measurement to include algorithmic efficiency. Model compression techniques (pruning, quantization, knowledge distillation) often achieve greater energy savings than hardware improvements alone. MobileNet architectures achieve approximately `{python} mobilenet_flops_ratio_str` $\times$ fewer FLOPs versus ResNet, translating to proportional energy reduction on hardware that efficiently exploits the smaller model [@howard2017mobilenets]. These techniques, detailed in @sec-model-compression, establish that energy-aware benchmarking must evaluate algorithmic efficiency alongside hardware power consumption; the specific energy breakdown of INT8 versus FP32 is quantified in @sec-benchmarking-training-metrics-0f1a. As AI systems scale, this lesson becomes central to sustainable computing practices. ### Domain-Specific Benchmarks {#sec-benchmarking-domainspecific-benchmarks-b15f} @@ -409,7 +410,7 @@ This principle manifests concretely in practice through *Goodhart's Law in actio **Scenario**: You optimize a translation model for **BLEU score**. * **Original Model**: BLEU = 28.0, Inference = 50ms. -* **Optimized Model**: BLEU = 28.5 (Better!), Inference = 200 ms (4$\times$ slower). +* **Optimized Model**: BLEU = 28.5 (Better!), Inference = 200 ms (4 $\times$ slower). **The Math**: @@ -442,8 +443,8 @@ When evaluating hardware or software based on vendor-reported benchmarks, ask th **What is measured?** - "10ms inference latency"—is this model-only, or including preprocessing/postprocessing? -- "1000 TOPS"—at what precision? INT4 TOPS are 4$\times$ INT8 TOPS on the same hardware -- "2$\times$ faster than competitor"—on which workload? What batch size? What precision? +- "1000 TOPS"—at what precision? INT4 TOPS are 4 $\times$ INT8 TOPS on the same hardware +- "2 $\times$ faster than competitor"—on which workload? What batch size? What precision? **What is excluded?** @@ -461,19 +462,19 @@ When evaluating hardware or software based on vendor-reported benchmarks, ask th **Translation guide for common claims:** -| **Vendor Claim** | **What It Often Means** | -|:-------------------------------|:-------------------------------------------------------------------| -| **"Up to 10,000 images/sec"** | Peak throughput at maximum batch size, INT8, without preprocessing | -| **"Sub-millisecond latency"** | Accelerator compute only, excluding data transfer | -| **"5$\times$ more efficient"** | Per-operation efficiency, not total system efficiency | -| **"Optimized for AI"** | May only accelerate specific operations or precisions | +| **Vendor Claim** | **What It Often Means** | +|:--------------------------------|:-------------------------------------------------------------------| +| **"Up to 10,000 images/sec"** | Peak throughput at maximum batch size, INT8, without preprocessing | +| **"Sub-millisecond latency"** | Accelerator compute only, excluding data transfer | +| **"5 $\times$ more efficient"** | Per-operation efficiency, not total system efficiency | +| **"Optimized for AI"** | May only accelerate specific operations or precisions | ::: The underlying hardware infrastructure (CPUs, GPUs, TPUs[^fn-bench-tpu], and ASICs[^fn-asic]) determines the speed, efficiency, and scalability of ML systems. System benchmarks establish standardized methodologies for evaluating hardware performance across AI workloads, measuring metrics including computational throughput, memory bandwidth, power efficiency, and scaling characteristics [@reddi2020mlperf; @mattson2020mlperf]. [^fn-bench-tpu]: **Tensor Processing Unit (TPU)**: Google's custom ASIC for neural network workloads (see @sec-hardware-acceleration for architecture details). A single TPU v4 pod (4,096 chips) delivers up to 1.1 exaFLOPS of peak BF16 performance, making TPU benchmarking results an important reference point for specialized AI hardware. -[^fn-asic]: **Application-Specific Integrated Circuit (ASIC)**: Custom chips designed for specific computational tasks, offering 100–1000$\times$ better efficiency than CPUs for their target applications but lacking flexibility for other workloads. See @sec-hardware-acceleration for a detailed comparison of ASICs, GPUs, and FPGAs. +[^fn-asic]: **Application-Specific Integrated Circuit (ASIC)**: Custom chips designed for specific computational tasks, offering 100–1000 $\times$ better efficiency than CPUs for their target applications but lacking flexibility for other workloads. See @sec-hardware-acceleration for a detailed comparison of ASICs, GPUs, and FPGAs. System benchmarks serve two functions. For practitioners, they enable informed hardware selection by providing comparative data across configurations. For manufacturers, they quantify generational improvements and guide accelerator development. The co-evolution has been dramatic: as GPU adoption grew, accuracy improved rapidly, demonstrating that hardware and algorithmic advances drive progress in tandem. @@ -589,6 +590,7 @@ This quantitative analysis, formalized in roofline models[^fn-roofline-model], g \index{Roofline Model!etymology and origin} \index{Williams, Samuel!roofline model developer} + [^fn-roofline-model]: **Roofline Model**: Named for its visual appearance resembling a house roofline, this performance model was developed by Samuel Williams at UC Berkeley in 2009. The plot reveals whether workloads are compute-bound (sloped region, limited by peak FLOPS) or memory-bound (flat region, limited by bandwidth). The "ridge point" where the slope meets the flat line marks the critical arithmetic intensity threshold. The architectural metaphor is apt: performance cannot exceed the roofline, just as you cannot build above a roof. \index{BERT!inference deployment prediction} @@ -744,7 +746,7 @@ GPU utilization = `{python} bert_perf_b1_str` ÷ `{python} a100_tflops_fp16_str` Increase batch size to `{python} batch32_str`: -- Same `{python} bert_weight_mb_str` MB of weights, but `{python} batch32_str`$\times$ more compute +- Same `{python} bert_weight_mb_str` MB of weights, but `{python} batch32_str` $\times$ more compute - New FLOPs: `{python} bert_b32_flops_eq_str` = `{python} bert_batch32_flops_str` $\times$ 10⁹ - New intensity: `{python} bert_batch32_flops_str` $\times$ 10⁹ ÷ `{python} bert_weight_mb_str` $\times$ 10⁶ = `{python} bert_ai_b32_str` FLOPs/byte @@ -992,7 +994,7 @@ Measuring these operations correctly requires discipline. The following *micro-b To avoid measuring hardware artifacts instead of kernel performance, follow the **Systems Detective's Rules**: \index{DVFS!warm-up measurement artifact} -1. **The Warm-up Rule**: Never measure the first 10–50 iterations. Modern hardware uses **DVFS (Dynamic Voltage and Frequency Scaling)** and **Turbo Boost**. A "cold" GPU may take 100 ms to ramp from 300 MHz to 1.5 GHz. Your first batch will appear 5$\times$ slower than reality. +1. **The Warm-up Rule**: Never measure the first 10–50 iterations. Modern hardware uses **DVFS (Dynamic Voltage and Frequency Scaling)** and **Turbo Boost**. A "cold" GPU may take 100 ms to ramp from 300 MHz to 1.5 GHz. Your first batch will appear 5 $\times$ slower than reality. 2. **The Variance Rule**: Report the **Coefficient of Variation (CV)** ($CV = \sigma / \mu$). If $CV > 0.05$ (5%), your measurement is noisy. This usually indicates background OS jitter, thermal throttling\index{Thermal Throttling!impact on sustained perf}, or memory contention. \index{Speed of Light Check!utilization diagnosis} 3. **The "Speed of Light" (SOL) Check**: Compare your achieved throughput against the roofline. If your kernel achieves 10 TFLOPS on an H100 (peak ~`{python} h100_tflops_fp16_str` TFLOPS FP16, or ~`{python} h100_tflops_fp8_str` TFLOPS FP8 dense), do not just optimize the code; ask *why* the utilization is so low. Is it a kernel launch latency issue (too many small kernels)? @@ -1340,7 +1342,7 @@ A task definition is only as good as the data used to evaluate it. Standardized \index{GLUE!language understanding benchmark} In computer vision, ImageNet [@imagenet_website] [@deng2009imagenet], COCO [@lin2014microsoft] [@lin2014microsoft], and CIFAR-10 [@cifar10_website][^fn-cifar-10] [@krizhevsky2009learning] serve as reference standards; in natural language processing, SQuAD [@squad_website][^fn-squad] [@rajpurkar2016squad], GLUE [@wang2018glue][^fn-glue] [@wang2018glue], and WikiText [@wikitext_website] [@merity2016pointer] fulfill similar roles, each encompassing a range of complexities and edge cases. -[^fn-cifar-10]: **CIFAR-10**: A dataset of 60,000 32$\times$32 color images across 10 classes (airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck), collected by Alex Krizhevsky, Vinod Nair, and Geoffrey Hinton at the University of Toronto in 2009. Despite its small image size, CIFAR-10 became a standard reference for comparing deep learning architectures, with top-1 error rates improving from 18.5% with traditional methods to 2.6% with modern deep networks. +[^fn-cifar-10]: **CIFAR-10**: A dataset of 60,000 32 $\times$ 32 color images across 10 classes (airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck), collected by Alex Krizhevsky, Vinod Nair, and Geoffrey Hinton at the University of Toronto in 2009. Despite its small image size, CIFAR-10 became a standard reference for comparing deep learning architectures, with top-1 error rates improving from 18.5% with traditional methods to 2.6% with modern deep networks. [^fn-squad]: **SQuAD**: Stanford Question Answering Dataset, introduced in 2016, containing 100,000+ question-answer pairs based on Wikipedia articles. SQuAD became the gold standard for evaluating reading comprehension, with human performance at 86.8% F1 score and leading AI systems achieving over 90% by 2018, marking the first time machines exceeded human performance on this benchmark. @@ -1366,6 +1368,7 @@ Once the architecture is selected, model development follows two parallel optimi Evaluation metrics[^fn-metric-etymology] translate raw model behavior into numbers that can be compared, ranked, and used to make engineering decisions. The challenge is choosing the *right* numbers: a metric that captures accuracy but ignores latency may declare the winner to be a model too slow for production; one that rewards throughput but ignores energy may optimize for a deployment budget that does not exist. \index{Metric!etymology} + [^fn-metric-etymology]: **Metric**: From Greek "metron" (measure), the root of "meter," "geometry," and "symmetry." In mathematics, a metric is any function that defines distance between elements. ML borrowed this term broadly to mean any quantitative measure of model behavior. The Greek heritage reminds us that meaningful measurement requires both a standard unit and a consistent method, principles that benchmarking formalizes. Organizing these metrics into a coherent taxonomy helps practitioners select the right measurements for their evaluation goals. {#sec-benchmarking-metric-taxonomy-d4cd} @tbl-metric-taxonomy categorizes metrics by what each measures and when it should be applied: @@ -1459,10 +1462,10 @@ Producing benchmark numbers is the easy part; interpreting them correctly is whe | **INT8** | 35 ms | 71.2% | 3.5 | **Interpretation**: -The 3.4$\times$ speedup and 4$\times$ size reduction from quantization come at a minimal cost of 0.6% accuracy drop. For a battery-powered real-time system, INT8 is the clear choice, enabling 28 FPS processing compared to 8 FPS with FP32. +The 3.4 $\times$ speedup and 4 $\times$ size reduction from quantization come at a minimal cost of 0.6% accuracy drop. For a battery-powered real-time system, INT8 is the clear choice, enabling 28 FPS processing compared to 8 FPS with FP32. ::: -Before drawing conclusions from benchmark results, apply the vendor claim analysis framework introduced earlier (see the "Decoding Vendor Benchmark Claims" checklist) and extend it with two additional dimensions. First, *is the comparison fair?* Comparing ResNet-50 against MobileNet conflates architecture differences with optimization choices; precision differences (FP32 vs. INT8) alone can explain 2–4$\times$ performance gaps, and batch size, hardware generation, and software framework must all be controlled. Second, *are the statistics meaningful?* Reliable results require multiple runs (minimum 5, preferably 10+), reported variance with confidence intervals, clear handling of outliers, and steady-state operation rather than cold-start effects. +Before drawing conclusions from benchmark results, apply the vendor claim analysis framework introduced earlier (see the "Decoding Vendor Benchmark Claims" checklist) and extend it with two additional dimensions. First, *is the comparison fair?* Comparing ResNet-50 against MobileNet conflates architecture differences with optimization choices; precision differences (FP32 vs. INT8) alone can explain 2–4 $\times$ performance gaps, and batch size, hardware generation, and software framework must all be controlled. Second, *are the statistics meaningful?* Reliable results require multiple runs (minimum 5, preferably 10+), reported variance with confidence intervals, clear handling of outliers, and steady-state operation rather than cold-start effects. Applying these questions to *interpreting a benchmark claim* illustrates how incomplete specifications obscure real performance. @@ -1472,7 +1475,7 @@ Applying these questions to *interpreting a benchmark claim* illustrates how inc **Critical Questions**: 1. **What batch size?** Batch 256 achieves high throughput but 256ms latency; batch 1 achieves low latency but lower throughput. -2. **What precision?** INT8 is 2–4$\times$ faster than FP32 but may have accuracy implications. +2. **What precision?** INT8 is 2–4 $\times$ faster than FP32 but may have accuracy implications. 3. **What is included?** Pure inference, or including preprocessing? 4. **What accuracy?** Matching the original 76.1% Top-1, or degraded? @@ -1493,15 +1496,15 @@ Which of these metrics matters most depends entirely on the deployment context. Neural network compression—pruning, quantization, knowledge distillation, and architecture optimization—requires specialized benchmarks because compression reshapes the trade-off landscape: every byte saved or operation eliminated must be weighed against potential accuracy loss and hardware compatibility. -The most basic compression metric is raw size reduction: parameter count, memory footprint in bytes, and compressed storage requirements. But size alone is misleading. MobileNetV2 achieves approximately 72% ImageNet top-1 accuracy with `{python} mobilenet_params_m_str` million parameters versus ResNet-50's 76% accuracy with `{python} resnet50_params_m_str` million parameters—a 7.5$\times$ efficiency improvement in the parameter-to-accuracy ratio that matters far more than raw parameter counts. +The most basic compression metric is raw size reduction: parameter count, memory footprint in bytes, and compressed storage requirements. But size alone is misleading. MobileNetV2 achieves approximately 72% ImageNet top-1 accuracy with `{python} mobilenet_params_m_str` million parameters versus ResNet-50's 76% accuracy with `{python} resnet50_params_m_str` million parameters—a 7.5 $\times$ efficiency improvement in the parameter-to-accuracy ratio that matters far more than raw parameter counts. -Pruning benchmarks must distinguish between structured and unstructured approaches, because they behave very differently on real hardware. Structured pruning removes entire neurons or filters, achieving consistent speedups but typically lower compression ratios (2–4$\times$). Unstructured pruning eliminates individual weights for higher compression ratios (10–100$\times$), but realizing actual speedups requires specialized sparse computation support—meaning benchmark protocols must specify hardware platform and software implementation. +Pruning benchmarks must distinguish between structured and unstructured approaches, because they behave very differently on real hardware. Structured pruning removes entire neurons or filters, achieving consistent speedups but typically lower compression ratios (2–4 $\times$). Unstructured pruning eliminates individual weights for higher compression ratios (10–100 $\times$), but realizing actual speedups requires specialized sparse computation support—meaning benchmark protocols must specify hardware platform and software implementation. \index{Mixed-Precision!layer-wise precision assignment} \index{Knowledge Distillation!benchmark evaluation} -Quantization benchmarks evaluate precision reduction across data types. INT8 quantization reduces memory footprint by 4$\times$ and accelerates inference by 2–4$\times$ (the precision-accuracy trade-off is analyzed in @sec-benchmarking-inference-metrics-78d4, and the energy implications in @sec-benchmarking-training-metrics-0f1a). Mixed-precision approaches push further by applying different precision levels to different layers—critical layers retain FP16 while computation-heavy layers use INT8 or INT4—enabling fine-grained efficiency optimization. Knowledge distillation adds another dimension: successful transfer achieves 90–95% of the teacher model's accuracy while reducing size by 5–10$\times$, but benchmarking must verify that the student generalizes rather than merely memorizing the teacher's outputs. +Quantization benchmarks evaluate precision reduction across data types. INT8 quantization reduces memory footprint by 4 $\times$ and accelerates inference by 2–4 $\times$ (the precision-accuracy trade-off is analyzed in @sec-benchmarking-inference-metrics-78d4, and the energy implications in @sec-benchmarking-training-metrics-0f1a). Mixed-precision approaches push further by applying different precision levels to different layers—critical layers retain FP16 while computation-heavy layers use INT8 or INT4—enabling fine-grained efficiency optimization. Knowledge distillation adds another dimension: successful transfer achieves 90–95% of the teacher model's accuracy while reducing size by 5–10 $\times$, but benchmarking must verify that the student generalizes rather than merely memorizing the teacher's outputs. -Critically, acceleration factors vary dramatically across hardware platforms: sparse models deliver 2–5$\times$ speedup on CPUs, reduced-precision models achieve 2–8$\times$ on mobile processors, and efficient architectures provide 5–20$\times$ on specialized edge accelerators. Current benchmark suites like MLPerf focus primarily on dense, unoptimized models that do not represent production deployments, where compressed models are ubiquitous. This gap between what benchmarks measure and what production actually runs remains one of the field's most consequential blind spots. +Critically, acceleration factors vary dramatically across hardware platforms: sparse models deliver 2–5 $\times$ speedup on CPUs, reduced-precision models achieve 2–8 $\times$ on mobile processors, and efficient architectures provide 5–20 $\times$ on specialized edge accelerators. Current benchmark suites like MLPerf focus primarily on dense, unoptimized models that do not represent production deployments, where compressed models are ubiquitous. This gap between what benchmarks measure and what production actually runs remains one of the field's most consequential blind spots. ### Mobile and Edge Benchmarks {#sec-benchmarking-mobile-edge-benchmarks-fd4f} @@ -1547,7 +1550,7 @@ When evaluating edge hardware claims: #### Heterogeneous Processor Coordination {#sec-benchmarking-heterogeneous-processor-coordination-68cf} -Mobile SoCs integrate heterogeneous processors (CPU, GPU, DSP, NPU) requiring specialized benchmarking that captures workload distribution complexity while accounting for thermal and battery constraints. Effective processor coordination achieves 3–5$\times$ performance improvements through intelligent work distribution.\index{NPU!neural network acceleration} Each processor excels at different workload profiles: CPUs handle control flow, small batches, and sequential processing; GPUs accelerate parallel floating-point operations and general ML inference; DSPs excel at fixed-point signal processing and always-on detection tasks; and NPUs target specific neural network architectures with INT8/INT4 precision. +Mobile SoCs integrate heterogeneous processors (CPU, GPU, DSP, NPU) requiring specialized benchmarking that captures workload distribution complexity while accounting for thermal and battery constraints. Effective processor coordination achieves 3–5 $\times$ performance improvements through intelligent work distribution.\index{NPU!neural network acceleration} Each processor excels at different workload profiles: CPUs handle control flow, small batches, and sequential processing; GPUs accelerate parallel floating-point operations and general ML inference; DSPs excel at fixed-point signal processing and always-on detection tasks; and NPUs target specific neural network architectures with INT8/INT4 precision. Benchmarks must evaluate workload placement decisions, not just individual processor performance. A voice assistant, for example, might use the DSP for always-on wake-word detection (5 mW continuous), switch to the NPU for speech recognition (200 mW burst), and use the CPU for language understanding (100 mW). Single-processor benchmarks miss these orchestration dynamics entirely. @@ -1572,7 +1575,7 @@ Whether benchmarking cloud servers or microcontrollers, however, a critical dist Training and inference pursue fundamentally different objectives, and these contrasting goals create evaluation requirements so different that separate benchmarking frameworks emerged for each: MLPerf Training and MLPerf Inference. The following sections detail how each framework validates the hardware acceleration claims from preceding chapters, revealing whether theoretical TFLOPS translate to practical time-to-train or queries-per-second. Training seeks optimal parameters through iterative refinement (@sec-model-training), processing billions of examples over hours or days, stressing memory bandwidth, multi-GPU scaling, and sustained throughput. Inference applies those parameters to individual inputs under deployment strategies (@sec-ml-operations), often within millisecond deadlines, stressing latency consistency, cold-start time, and power efficiency. -The differences cascade through every aspect of system design. Training involves bidirectional computation (forward and backward passes), while inference performs single forward passes with fixed parameters. Memory allocation diverges sharply: training requires simultaneous access to parameters, gradients, optimizer states, and activations, creating 3–4$\times$ memory overhead compared to inference. Training employs mixed-precision computation and gradient compression to manage this overhead, while inference leverages more aggressive precision reduction (detailed in @sec-benchmarking-inference-metrics-78d4) and techniques like post-training quantization and knowledge distillation. Resource utilization patterns also contrast: training targets sustained GPU saturation, whereas inference contends with variable request patterns that leave hardware underutilized, as the roofline analysis in @sec-benchmarking-system-benchmarks-393c demonstrated. +The differences cascade through every aspect of system design. Training involves bidirectional computation (forward and backward passes), while inference performs single forward passes with fixed parameters. Memory allocation diverges sharply: training requires simultaneous access to parameters, gradients, optimizer states, and activations, creating 3–4 $\times$ memory overhead compared to inference. Training employs mixed-precision computation and gradient compression to manage this overhead, while inference leverages more aggressive precision reduction (detailed in @sec-benchmarking-inference-metrics-78d4) and techniques like post-training quantization and knowledge distillation. Resource utilization patterns also contrast: training targets sustained GPU saturation, whereas inference contends with variable request patterns that leave hardware underutilized, as the roofline analysis in @sec-benchmarking-system-benchmarks-393c demonstrated. Energy costs follow different patterns. Training energy costs are amortized across model lifetime and measured in total energy per trained model; estimates for large training runs can reach the scale of thousands of megawatt-hours (GPT-3 has been estimated at roughly 1,287 MWh) [@patterson2021carbon]. Inference energy costs accumulate per query and can become a dominant operational consideration at scale. A durable way to reason about per-query energy is the identity \(E = P \times t\). For example, a `{python} accel_power_w_str` W accelerator running a `{python} latency_fast_ms_str` ms inference consumes \(`{python} accel_power_w_str` \times 0.01 = `{python} energy_fast_j_str`\) joules, which is about \(`{python} energy_fast_wh_str`\) Wh; at `{python} latency_slow_ms_str` ms, that becomes about \(`{python} energy_slow_wh_str`\) Wh. @@ -1586,7 +1589,7 @@ Training benchmarks divide into three categories: convergence metrics that measu Training benchmarks validate whether hardware acceleration delivers promised training throughput. The GPU clusters, TPU pods, and distributed training strategies examined in @sec-hardware-acceleration all claim dramatic speedups, and training benchmarks reveal which claims hold under realistic workloads. They evaluate how hardware configurations, data loading mechanisms, and distributed training strategies actually perform when training production-scale models. -These benchmarks are vital because training represents the largest capital expenditure in ML systems. A cluster that costs $10M should demonstrably outperform a $2M cluster on training time-to-accuracy, but only rigorous benchmarking reveals whether the 5$\times$ cost delivers proportional value or falls victim to scaling inefficiencies, memory bottlenecks, or communication overhead. +These benchmarks are vital because training represents the largest capital expenditure in ML systems. A cluster that costs $10M should demonstrably outperform a $2M cluster on training time-to-accuracy, but only rigorous benchmarking reveals whether the 5 $\times$ cost delivers proportional value or falls victim to scaling inefficiencies, memory bottlenecks, or communication overhead. For instance, large-scale models like OpenAI's GPT-3[^fn-bench-gpt3] [@brown2020language], which consists of `{python} gpt3_params_b_str` billion parameters trained on approximately 570GB of filtered CommonCrawl text (from a ~45TB raw dataset, combined with other sources to form `{python} gpt3_tokens_b_str` billion training tokens), highlight the immense computational demands of modern training. Standardized *ML training benchmarks* provide systematic evaluation of the underlying systems to ensure that hardware and software configurations can meet these unprecedented demands efficiently. @@ -1604,9 +1607,9 @@ MLPerf Training [@mlperf_training_website] provides the standardized framework r ### Training Benchmark Motivation {#sec-benchmarking-training-benchmark-motivation-f365} The impact of standardized training measurement is striking. \index{Moore's Law!outpaced by ML training improvements} -@fig-mlperf-training-improve demonstrates that performance improvements across successive MLPerf Training benchmark versions have consistently outpaced Moore's Law, with ResNet training speedups exceeding 30$\times$ over five years while semiconductor scaling would predict only 6.6$\times$. This exponential improvement illustrates a core principle: what gets measured gets improved. The standardized benchmarking framework creates competitive pressure that drives rapid optimization across the entire ML computing stack. +@fig-mlperf-training-improve demonstrates that performance improvements across successive MLPerf Training benchmark versions have consistently outpaced Moore's Law, with ResNet training speedups exceeding 30 $\times$ over five years while semiconductor scaling would predict only 6.6 $\times$. This exponential improvement illustrates a core principle: what gets measured gets improved. The standardized benchmarking framework creates competitive pressure that drives rapid optimization across the entire ML computing stack. -::: {#fig-mlperf-training-improve fig-env="figure" fig-pos="htb" fig-cap="**MLPerf Training Progress**: Standardized benchmarks reveal that machine learning training performance consistently surpasses Moore's Law, indicating substantial gains from systems-level optimizations. These trends emphasize how focused measurement and iterative improvement drive rapid advancements in ML training efficiency and scalability. Source: [@tschand2024mlperf]." fig-alt="Line chart with nine model benchmarks from 2018 to 2024 showing relative performance gains up to 48× for Mask R-CNN, all exceeding the Moore's Law baseline of 6.6×."} +::: {#fig-mlperf-training-improve fig-env="figure" fig-pos="htb" fig-cap="**MLPerf Training Progress**: Standardized benchmarks reveal that machine learning training performance consistently surpasses Moore's Law, indicating substantial gains from systems-level optimizations. These trends emphasize how focused measurement and iterative improvement drive rapid advancements in ML training efficiency and scalability. Source: [@tschand2024mlperf]." fig-alt="Line chart with nine model benchmarks from 2018 to 2024 showing relative performance gains up to 48 $\times$ for Mask R-CNN, all exceeding the Moore's Law baseline of 6.6 $\times$."} ```{.tikz} \begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}] %\node[anchor=south west]at(-0.4,-12){% @@ -1800,7 +1803,7 @@ Training benchmarks drive progress because the exponential improvements shown in \index{Mixed-Precision Training!FP16 and FP32} Training benchmarks serve four interconnected functions. First, they enable hardware and software optimization by providing vendor-neutral comparisons across accelerator architectures and frameworks (TensorFlow, PyTorch) on standardized tasks, guiding hardware selection for data centers and cloud environments. Software optimizations including mixed-precision training[^fn-bench-mixed-precision] and memory-efficient data loading are similarly quantified. Second, they evaluate scalability: adding GPUs should reduce training time proportionally, but communication overhead, synchronization latency, and memory bottlenecks limit scaling efficiency in practice. Training benchmarks quantify these losses, revealing whether infrastructure investments deliver proportional returns. Third, they provide cost and energy accountability: with large-scale training runs consuming thousands of megawatt-hours, benchmarks that track cost per training run and power consumption per unit of progress help organizations balance computational power with sustainability goals. Finally, they ensure fair, reproducible comparison through standardized evaluation criteria, controlled randomness, and strict submission guidelines that guarantee performance results reflect genuine system capabilities rather than implementation-specific tuning. -[^fn-bench-mixed-precision]: **Mixed-Precision Training**: A training technique that uses both 16-bit (FP16) and 32-bit (FP32) floating-point representations to accelerate training while maintaining model accuracy. Introduced by NVIDIA in 2017, mixed precision can achieve 1.5–2$\times$ speedups on modern GPUs with Tensor Cores while reducing memory usage by ~40%, enabling larger batch sizes and faster convergence for large models. +[^fn-bench-mixed-precision]: **Mixed-Precision Training**: A training technique that uses both 16-bit (FP16) and 32-bit (FP32) floating-point representations to accelerate training while maintaining model accuracy. Introduced by NVIDIA in 2017, mixed precision can achieve 1.5–2 $\times$ speedups on modern GPUs with Tensor Cores while reducing memory usage by ~40%, enabling larger batch sizes and faster convergence for large models. ### Training Metrics {#sec-benchmarking-training-metrics-0f1a} @@ -1821,6 +1824,7 @@ This metric ensures that benchmarking focuses on how quickly and effectively a s Throughput[^fn-throughput-etymology], often expressed as the number of training samples processed per second, provides an additional measure of system performance. Let $N_{\text{samples}}$ be the total number of training samples processed and $T_{\text{train}}$ the training time from @eq-training-time-benchmark. @eq-throughput-benchmark shows: \index{Throughput!etymology} + [^fn-throughput-etymology]: **Throughput**: A compound of "through" and "put" (to place), originally from manufacturing where it measured units passing through a production line per unit time. The term entered computing in the 1960s batch-processing era to describe jobs completed per hour. In ML systems, throughput measures samples processed, tokens generated, or inferences completed per second, the rate at which work flows through the computational pipeline. $$\text{Throughput} = \frac{N_{\text{samples}}}{T_{\text{train}}}$$ {#eq-throughput-benchmark} @@ -1921,7 +1925,7 @@ The "missing" `{python} loss_str`% decomposes into measurable overhead: Scaling efficiency decreases as $N$ grows because communication overhead scales with GPU count while per-GPU compute shrinks. At 8 GPUs, 75% efficiency is typical. At 64 GPUs, efficiency often drops to 50-60%. At 1000+ GPUs, even 30-40% efficiency requires sophisticated optimization. -This is why MLPerf reports both raw performance AND scaling efficiency: a system achieving 2$\times$ throughput at 50% efficiency may be worse than 1.5$\times$ throughput at 90% efficiency, depending on your cost constraints. +This is why MLPerf reports both raw performance AND scaling efficiency: a system achieving 2 $\times$ throughput at 50% efficiency may be worse than 1.5 $\times$ throughput at 90% efficiency, depending on your cost constraints. ::: #### Resource Utilization {#sec-benchmarking-resource-utilization-336b} @@ -2021,32 +2025,32 @@ Recall from @sec-hardware-acceleration that moving data costs far more energy th | **Precision** | **Multiplier Energy** | **Relative Cost** | |:--------------|-------------------------------:|------------------:| -| **FP32** | ~`{python} energy_fp32_str` pJ | 1.0$\times$ | -| **FP16** | ~`{python} energy_fp16_str` pJ | 0.3$\times$ | -| **INT8** | ~`{python} energy_int8_str` pJ | 0.05$\times$ | +| **FP32** | ~`{python} energy_fp32_str` pJ | 1.0 $\times$ | +| **FP16** | ~`{python} energy_fp16_str` pJ | 0.3 $\times$ | +| **INT8** | ~`{python} energy_int8_str` pJ | 0.05 $\times$ | -An 8-bit multiplier uses ~20$\times$ less energy than a 32-bit floating-point multiplier because transistor count scales roughly with bit-width squared, and switching energy scales with transistor count. +An 8-bit multiplier uses ~20 $\times$ less energy than a 32-bit floating-point multiplier because transistor count scales roughly with bit-width squared, and switching energy scales with transistor count. **Memory Access Energy (per byte)**: | **Memory Level** | **Energy per Byte** | **Relative Cost** | |:-----------------|----------------------------------:|------------------:| -| **Register** | ~`{python} energy_reg_str` pJ | 1$\times$ | -| **L1 Cache** | ~`{python} energy_l1_str` pJ | 50$\times$ | -| **L2 Cache** | ~`{python} energy_l2_str` pJ | 200$\times$ | -| **DRAM** | ~`{python} dram_energy_pj_str` pJ | 16,000$\times$ | +| **Register** | ~`{python} energy_reg_str` pJ | 1 $\times$ | +| **L1 Cache** | ~`{python} energy_l1_str` pJ | 50 $\times$ | +| **L2 Cache** | ~`{python} energy_l2_str` pJ | 200 $\times$ | +| **DRAM** | ~`{python} dram_energy_pj_str` pJ | 16,000 $\times$ | -Memory access dominates: reading one byte from DRAM costs over 10,000$\times$ more energy than a register access. +Memory access dominates: reading one byte from DRAM costs over 10,000 $\times$ more energy than a register access. **Combined Effect for MobileNet Inference**: -| **Component** | **FP32 (`{python} m_fp32_mb_str` MB)** | **INT8 (`{python} m_int8_mb_str` MB)** | **Savings** | -|:-------------------------------------------|---------------------------------------:|---------------------------------------:|-----------------------------------:| -| **Model load from DRAM** | `{python} e_fp32_load_str` μJ | `{python} e_int8_load_str` μJ | `{python} s_load_str`$\times$ | -| **Compute (`{python} m_macs_str` M MACs)** | `{python} e_fp32_compute_str` μJ | `{python} e_int8_compute_str` μJ | `{python} s_compute_str`$\times$ | -| **Total** | **`{python} e_fp32_total_str` μJ** | **`{python} e_int8_total_str` μJ** | **`{python} s_total_str`$\times$** | +| **Component** | **FP32 (`{python} m_fp32_mb_str` MB)** | **INT8 (`{python} m_int8_mb_str` MB)** | **Savings** | +|:-------------------------------------------|---------------------------------------:|---------------------------------------:|------------------------------------:| +| **Model load from DRAM** | `{python} e_fp32_load_str` μJ | `{python} e_int8_load_str` μJ | `{python} s_load_str` $\times$ | +| **Compute (`{python} m_macs_str` M MACs)** | `{python} e_fp32_compute_str` μJ | `{python} e_int8_compute_str` μJ | `{python} s_compute_str` $\times$ | +| **Total** | **`{python} e_fp32_total_str` μJ** | **`{python} e_int8_total_str` μJ** | **`{python} s_total_str` $\times$** | -**The Systems Insight**: Memory access dominates FP32 energy consumption (~`{python} e_fp32_load_mj_str` mJ vs `{python} e_fp32_compute_mj_str` mJ compute). INT8 quantization provides `{python} s_load_str`$\times$ memory energy reduction and ~`{python} s_compute_str`$\times$ compute energy reduction. The combined effect explains why quantized models on edge devices achieve dramatic battery life improvements: they attack the dominant memory bottleneck while simultaneously accelerating compute. +**The Systems Insight**: Memory access dominates FP32 energy consumption (~`{python} e_fp32_load_mj_str` mJ vs `{python} e_fp32_compute_mj_str` mJ compute). INT8 quantization provides `{python} s_load_str` $\times$ memory energy reduction and ~`{python} s_compute_str` $\times$ compute energy reduction. The combined effect explains why quantized models on edge devices achieve dramatic battery life improvements: they attack the dominant memory bottleneck while simultaneously accelerating compute. ::: Training GPT-3 was estimated to consume `{python} gpt3_energy_mwh_str` MWh of electricity [@patterson2021carbon]. If a system can achieve the same accuracy with fewer training iterations, it directly reduces energy consumption. Energy-aware benchmarks help guide the development of hardware and training strategies that optimize power efficiency while maintaining accuracy targets. @@ -2131,9 +2135,9 @@ This is where the optimization chapters converge: the accelerated hardware from \index{NPU!inference benchmarking considerations} Unlike training, which runs on dedicated data center hardware, inference must be optimized for dramatically diverse deployment scenarios — from real-time applications like autonomous driving and conversational AI to mobile devices, IoT systems, and embedded processors. This diversity extends to hardware: while GPUs and TPUs dominate training, inference workloads often require specialized accelerators like NPUs[^fn-npu-benchmarking], FPGAs[^fn-fpga-benchmarking], and dedicated inference chips such as Google's Edge TPU[^fn-edge-tpu]. Inference benchmarks evaluate how well hardware selection, model optimization, and data pipeline design work together across these deployment environments. -[^fn-npu-benchmarking]: NPUs (introduced in @sec-ml-systems) require different benchmarking approaches than GPUs. Peak TOPS ratings (1–15 TOPS for mobile NPUs) vary by precision and operator; meaningful benchmarks must measure end-to-end model latency rather than synthetic peak throughput. Power consumption benchmarks are equally critical since NPUs target 100–1000$\times$ better energy efficiency than GPUs. +[^fn-npu-benchmarking]: NPUs (introduced in @sec-ml-systems) require different benchmarking approaches than GPUs. Peak TOPS ratings (1–15 TOPS for mobile NPUs) vary by precision and operator; meaningful benchmarks must measure end-to-end model latency rather than synthetic peak throughput. Power consumption benchmarks are equally critical since NPUs target 100–1000 $\times$ better energy efficiency than GPUs. -[^fn-fpga-benchmarking]: **Field-Programmable Gate Array (FPGA)**: Reconfigurable silicon chips that can be programmed after manufacturing to implement custom digital circuits. Unlike fixed ASICs, FPGAs offer flexibility to optimize for different algorithms, achieving 10–100$\times$ better energy efficiency than CPUs for specific ML workloads while maintaining adaptability to algorithm changes. +[^fn-fpga-benchmarking]: **Field-Programmable Gate Array (FPGA)**: Reconfigurable silicon chips that can be programmed after manufacturing to implement custom digital circuits. Unlike fixed ASICs, FPGAs offer flexibility to optimize for different algorithms, achieving 10–100 $\times$ better energy efficiency than CPUs for specific ML workloads while maintaining adaptability to algorithm changes. [^fn-edge-tpu]: **Edge TPU**: Google's ultra-low-power AI accelerator designed for edge devices, consuming only 2 watts while delivering 4 TOPS of performance. Each Edge TPU is optimized for TensorFlow Lite models and costs around $25, making distributed AI deployment economically viable at massive scale. @@ -2189,13 +2193,14 @@ MLPerf's inference benchmarks provide standardized evaluation across deployment \index{ML Compiler Stack!automated hardware optimization} Inference benchmarks evaluate the bottlenecks that emerge when models transition from development to production serving. The motivating factors parallel those for training (hardware optimization, scalability, cost, fair comparison) but differ in specifics. Software optimization frameworks like TensorRT[^fn-tensorrt], ONNX Runtime[^fn-onnx-runtime-inference], and TVM[^fn-tvm] apply inference-specific techniques — operator fusion[^fn-operator-fusion], precision calibration, and kernel tuning — whose impact on latency, throughput, and power efficiency must be measured under realistic conditions to confirm they deliver real improvements without degrading accuracy. -[^fn-tensorrt]: **TensorRT**: NVIDIA's high-performance inference optimizer and runtime library that accelerates deep learning models on NVIDIA GPUs. Introduced in 2016, TensorRT applies graph optimizations, kernel fusion, and precision calibration to achieve 1.5–7$\times$ speedups over naive implementations, supporting FP16, INT8, and sparse matrix operations. +[^fn-tensorrt]: **TensorRT**: NVIDIA's high-performance inference optimizer and runtime library that accelerates deep learning models on NVIDIA GPUs. Introduced in 2016, TensorRT applies graph optimizations, kernel fusion, and precision calibration to achieve 1.5–7 $\times$ speedups over naive implementations, supporting FP16, INT8, and sparse matrix operations. [^fn-onnx-runtime-inference]: **ONNX Runtime**: Microsoft's cross-platform, high-performance ML inferencing and training accelerator supporting the Open Neural Network Exchange (ONNX) format. Released in 2018, it enables models trained in any framework to run efficiently across different hardware (CPU, GPU, NPU) with optimizations like graph fusion and memory pattern optimization. [^fn-tvm]: **TVM**: An open-source deep learning compiler stack that optimizes tensor programs for diverse hardware backends including CPUs, GPUs, and specialized accelerators. Developed at the University of Washington, TVM uses machine learning to automatically generate optimized code, achieving performance competitive with hand-tuned libraries while supporting new hardware architectures. \index{Operator Fusion!compiler optimization technique} + [^fn-operator-fusion]: **Operator Fusion**: A compiler optimization technique that combines multiple neural network operations into single kernels to reduce memory bandwidth requirements and improve cache efficiency. For example, fusing convolution with batch normalization and ReLU can eliminate intermediate memory writes, achieving 20–40% speedups in inference workloads. \index{Cold-Start Performance!model loading latency} @@ -2238,7 +2243,7 @@ The metric shapes the optimization. - [ ] **End-to-End**: Does your "inference latency" include preprocessing? (If not, your benchmark is a lie). ::: -Tail latency's connection to user experience at scale becomes critical in production systems serving millions of users. Even small P99 latency degradations create compounding effects across large user bases: if 1% of requests experience 10$\times$ latency (e.g., 1000 ms instead of 100 ms), this affects 10,000 users per million requests, potentially leading to timeout errors, poor user experience, and customer churn. Search engines and recommendation systems demonstrate this sensitivity: industry studies have shown that latency increases on the order of hundreds of milliseconds can reduce engagement by 10–20% and conversions by measurable percentages, making sub-100 ms response times a common target for interactive services. +Tail latency's connection to user experience at scale becomes critical in production systems serving millions of users. Even small P99 latency degradations create compounding effects across large user bases: if 1% of requests experience 10 $\times$ latency (e.g., 1000 ms instead of 100 ms), this affects 10,000 users per million requests, potentially leading to timeout errors, poor user experience, and customer churn. Search engines and recommendation systems demonstrate this sensitivity: industry studies have shown that latency increases on the order of hundreds of milliseconds can reduce engagement by 10–20% and conversions by measurable percentages, making sub-100 ms response times a common target for interactive services. Service level objectives (SLOs) in production systems therefore focus on tail latency rather than mean latency to ensure consistent user experience. Typical production SLOs specify P95 < 100ms and P99 < 500ms for interactive services, recognizing that occasional slow responses have disproportionate impact on user satisfaction. Large-scale systems like Netflix and Uber optimize for P99.9 latency to handle traffic spikes and infrastructure variations that affect service reliability. @@ -2315,11 +2320,11 @@ preprocess_fraction_str = fmt(preprocess_fraction_value, precision=2, commas=Fal ::: {.callout-notebook title="Amdahl's Law: Optimization Ceiling"} -The latency breakdown reveals why aggressive model optimization often yields disappointing end-to-end results. Consider a vision pipeline where preprocessing (JPEG decode, resize, normalize) consumes `{python} bench_preprocess_ms_str` ms and inference consumes `{python} bench_inference_ms_str` ms. Optimizing inference by `{python} bench_inf_speedup_str`$\times$ (from `{python} bench_inference_ms_str` ms to `{python} bench_opt_inf_str` ms) reduces total latency from `{python} bench_total_ms_str` ms to only `{python} bench_opt_total_str` ms, a `{python} bench_e2e_str`$\times$ improvement rather than `{python} bench_inf_speedup_str`$\times$. +The latency breakdown reveals why aggressive model optimization often yields disappointing end-to-end results. Consider a vision pipeline where preprocessing (JPEG decode, resize, normalize) consumes `{python} bench_preprocess_ms_str` ms and inference consumes `{python} bench_inference_ms_str` ms. Optimizing inference by `{python} bench_inf_speedup_str` $\times$ (from `{python} bench_inference_ms_str` ms to `{python} bench_opt_inf_str` ms) reduces total latency from `{python} bench_total_ms_str` ms to only `{python} bench_opt_total_str` ms, a `{python} bench_e2e_str` $\times$ improvement rather than `{python} bench_inf_speedup_str` $\times$. -Amdahl's Law formalizes this ceiling: if preprocessing consumes fraction f of total latency, then even infinitely fast inference yields at most 1/f speedup. With preprocessing at `{python} preprocess_pct_str`% of latency (f ≈ `{python} preprocess_fraction_str`), the maximum achievable speedup is 1/f ≈ `{python} amdahl_ceiling_str`$\times$ regardless of model optimization. +Amdahl's Law formalizes this ceiling: if preprocessing consumes fraction f of total latency, then even infinitely fast inference yields at most 1/f speedup. With preprocessing at `{python} preprocess_pct_str`% of latency (f ≈ `{python} preprocess_fraction_str`), the maximum achievable speedup is 1/f ≈ `{python} amdahl_ceiling_str` $\times$ regardless of model optimization. -This principle has direct implications for benchmarking interpretation. A 3$\times$ inference speedup reported in isolation might translate to only 1.5$\times$ end-to-end improvement in production. Comprehensive benchmarks must either include preprocessing in measurements or clearly state that reported speedups apply only to the inference component. +This principle has direct implications for benchmarking interpretation. A 3 $\times$ inference speedup reported in isolation might translate to only 1.5 $\times$ end-to-end improvement in production. Comprehensive benchmarks must either include preprocessing in measurements or clearly state that reported speedups apply only to the inference component. ::: @@ -2348,17 +2353,17 @@ Benchmarks must consider both single-instance and batch throughput to provide a #### Precision and Accuracy Trade-offs {#sec-benchmarking-precision-accuracy-tradeoffs-3bbb} -Optimizing inference performance often involves reducing numerical precision, which can accelerate computation by 2–4$\times$ while reducing memory and energy consumption. However, lower-precision calculations can introduce accuracy degradation, making it essential to benchmark the trade-offs between speed and predictive quality. +Optimizing inference performance often involves reducing numerical precision, which can accelerate computation by 2–4 $\times$ while reducing memory and energy consumption. However, lower-precision calculations can introduce accuracy degradation, making it essential to benchmark the trade-offs between speed and predictive quality. Inference benchmarks evaluate how well models perform under different numerical settings, such as FP32[^fn-fp32], FP16[^fn-fp16], and INT8[^fn-int8]. Many modern AI accelerators support mixed-precision inference, allowing systems to dynamically adjust numerical representation based on workload requirements. Model compression techniques[^fn-model-compression-benchmarking] further improve efficiency, but their impact on model accuracy varies depending on the task and dataset. Benchmarks help determine whether these optimizations are viable for deployment, ensuring that improvements in efficiency do not come at the cost of unacceptable accuracy loss. [^fn-fp32]: **FP32**: 32-bit floating-point format providing high numerical precision with approximately 7 decimal digits of accuracy. Standard for research and training, FP32 operations consume maximum memory and computational resources but ensure numerical stability. Modern GPUs achieve `{python} v100_tflops_fp32_str`–`{python} a100_tflops_fp32_str` TFLOPS in FP32, serving as the baseline for precision comparisons. -[^fn-fp16]: **FP16**: 16-bit floating-point format that halves memory usage compared to FP32 while maintaining reasonable numerical precision. Widely supported by modern AI accelerators, FP16 can achieve 2–4$\times$ speedups over FP32 with minimal accuracy loss for most deep learning models, making it the preferred format for inference and mixed-precision training. +[^fn-fp16]: **FP16**: 16-bit floating-point format that halves memory usage compared to FP32 while maintaining reasonable numerical precision. Widely supported by modern AI accelerators, FP16 can achieve 2–4 $\times$ speedups over FP32 with minimal accuracy loss for most deep learning models, making it the preferred format for inference and mixed-precision training. -[^fn-int8]: **INT8**: 8-bit integer format providing maximum memory and computational efficiency, requiring only 25% of FP32 storage. Post-training precision reduction to INT8 can achieve 4$\times$ memory reduction and 2–4$\times$ speedup on specialized hardware, but requires careful calibration to minimize accuracy degradation, typically maintaining 95–99% of original model performance. +[^fn-int8]: **INT8**: 8-bit integer format providing maximum memory and computational efficiency, requiring only 25% of FP32 storage. Post-training precision reduction to INT8 can achieve 4 $\times$ memory reduction and 2–4 $\times$ speedup on specialized hardware, but requires careful calibration to minimize accuracy degradation, typically maintaining 95–99% of original model performance. -[^fn-model-compression-benchmarking]: Model compression (detailed in @sec-model-compression) encompasses precision reduction, structural optimization, and knowledge transfer. For benchmarking, compression impact must be measured across multiple dimensions: accuracy degradation, inference speedup, memory reduction, and energy savings. A technique achieving 10$\times$ size reduction with only 1% accuracy loss may still be unsuitable if latency does not improve proportionally. +[^fn-model-compression-benchmarking]: Model compression (detailed in @sec-model-compression) encompasses precision reduction, structural optimization, and knowledge transfer. For benchmarking, compression impact must be measured across multiple dimensions: accuracy degradation, inference speedup, memory reduction, and energy savings. A technique achieving 10 $\times$ size reduction with only 1% accuracy loss may still be unsuitable if latency does not improve proportionally. #### Memory Footprint and Model Size {#sec-benchmarking-memory-footprint-model-size-bf31} @@ -2421,7 +2426,7 @@ Different deployment scenarios require distinctly different metric priorities, a : **Performance Metric Priorities by Deployment Context.** Different operational environments demand distinct optimization focuses, reflecting varying constraints and success criteria. These priorities guide both benchmark selection and result interpretation. {#tbl-metric-priorities} -The key insight from @tbl-metric-priorities is that the *same metric* can be primary in one context and irrelevant in another. Latency ranks first for real-time applications (autonomous vehicles must process sensor data within strict timing deadlines) but tertiary for cloud services (which accept higher latency in exchange for cost efficiency per query). A smartphone AI assistant that improves throughput by 50% but increases power consumption by 30% represents a net regression since battery life directly impacts user satisfaction. Medical diagnostic systems prioritize accuracy as non-negotiable — achieving 99.2% accuracy at 10 ms latency provides superior value compared to 98.8% at 5 ms. This context-dependence means that a 2$\times$ throughput improvement represents substantial value for cloud deployments but minimal benefit for battery-powered edge devices, where 20% power reduction delivers superior operational impact. +The key insight from @tbl-metric-priorities is that the *same metric* can be primary in one context and irrelevant in another. Latency ranks first for real-time applications (autonomous vehicles must process sensor data within strict timing deadlines) but tertiary for cloud services (which accept higher latency in exchange for cost efficiency per query). A smartphone AI assistant that improves throughput by 50% but increases power consumption by 30% represents a net regression since battery life directly impacts user satisfaction. Medical diagnostic systems prioritize accuracy as non-negotiable — achieving 99.2% accuracy at 10 ms latency provides superior value compared to 98.8% at 5 ms. This context-dependence means that a 2 $\times$ throughput improvement represents substantial value for cloud deployments but minimal benefit for battery-powered edge devices, where 20% power reduction delivers superior operational impact. Even with well-defined metrics, benchmarking inference systems can be challenging. Missteps during the evaluation process often lead to misleading conclusions. Students and practitioners should be aware of common pitfalls when analyzing inference performance. @@ -2443,7 +2448,7 @@ In serverless environments, where models are loaded on demand, cold-start latenc Benchmarking inference systems often involves balancing competing metrics. For example, maximizing batch throughput might degrade latency, while aggressive precision reduction could reduce accuracy. Focusing on a single metric without considering its impact on others can lead to incomplete or misleading evaluations. -Numerical precision optimization exemplifies this challenge particularly well. Individual accelerator benchmarks show INT8 operations achieving 4$\times$ higher TOPS[^fn-tops] (Tera Operations Per Second) compared to FP32, creating compelling performance narratives. +Numerical precision optimization exemplifies this challenge particularly well. Individual accelerator benchmarks show INT8 operations achieving 4 $\times$ higher TOPS[^fn-tops] (Tera Operations Per Second) compared to FP32, creating compelling performance narratives. [^fn-tops]: **TOPS (Tera Operations Per Second)**: A measure of computational throughput indicating trillions of operations per second, commonly used for AI accelerator performance. Modern AI chips achieve 100--1000 TOPS for INT8 operations, with the NVIDIA H100 delivering `{python} h100_tflops_int8_str` TOPS INT8, the Apple M2 Neural Engine providing 15.8 TOPS, and edge devices like Google Edge TPU achieving 4 TOPS. However, chip-level TOPS advantages often diminish at the system level due to convergence time, precision conversion overhead, and mixed-precision coordination, underscoring why isolated hardware metrics cannot substitute for holistic system evaluation. @@ -2467,7 +2472,7 @@ Avoiding these pitfalls requires treating benchmarking as a process of balancing \index{MLCommons!non-profit benchmark consortium} The MLPerf Inference benchmark, developed by MLCommons[^fn-mlcommons], provides a standardized framework for evaluating machine learning inference performance across a range of deployment environments. Initially, MLPerf started with a single inference benchmark, but as machine learning systems expanded into diverse applications, it became clear that a one-size-fits-all benchmark was insufficient. Different inference scenarios, including cloud-based AI services and resource-constrained embedded devices, demanded tailored evaluations. This realization led to the development of a family of MLPerf inference benchmarks, each designed to assess performance within a specific deployment setting. -[^fn-mlcommons]: **MLCommons**: Non-profit consortium (founded 2018 as MLPerf) establishing industry-standard ML benchmarks. Members include Google, NVIDIA, Intel, and leading universities. MLPerf Training measures time-to-accuracy; MLPerf Inference measures throughput/latency. Results reveal 10$\times$ performance differences between vendors, driving competitive innovation while enabling apples-to-apples hardware comparisons. +[^fn-mlcommons]: **MLCommons**: Non-profit consortium (founded 2018 as MLPerf) establishing industry-standard ML benchmarks. Members include Google, NVIDIA, Intel, and leading universities. MLPerf Training measures time-to-accuracy; MLPerf Inference measures throughput/latency. Results reveal 10 $\times$ performance differences between vendors, driving competitive innovation while enabling apples-to-apples hardware comparisons. #### MLPerf Inference {#sec-benchmarking-mlperf-inference-4b4a} @@ -2597,18 +2602,18 @@ Completing our MobileNet lighthouse example, we validate the hardware accelerati *Note: The following values are illustrative, based on typical EdgeTPU and Cortex-M7 performance characteristics. Actual results vary with clock frequency, thermal conditions, and specific implementation. Always benchmark your specific configuration.* -**Hardware acceleration claim**: EdgeTPU achieves ~`{python} edgetpu_latency_ms_str` ms inference for INT8 MobileNetV2, approximately `{python} inference_speedup_str`$\times$ speedup over ARM Cortex-M7 CPU (~`{python} cpu_latency_ms_str` ms). +**Hardware acceleration claim**: EdgeTPU achieves ~`{python} edgetpu_latency_ms_str` ms inference for INT8 MobileNetV2, approximately `{python} inference_speedup_str` $\times$ speedup over ARM Cortex-M7 CPU (~`{python} cpu_latency_ms_str` ms). **Validation protocol** (SingleStream scenario): -| **Metric** | **CPU (Cortex-M7)** | **EdgeTPU** | **Claimed** | **Validated?** | -|:-------------------------|----------------------------------:|--------------------------------------:|:------------------------------------------------|----------------------------------------------------:| -| **Inference latency** | ~`{python} cpu_latency_ms_str` ms | ~`{python} edgetpu_latency_ms_str` ms | `{python} inference_speedup_str`$\times$ faster | ✓ | -| **End-to-end latency** | ~`{python} cpu_e2e_ms_str` ms | ~`{python} edgetpu_e2e_ms_str` ms | — | ~`{python} e2e_speedup_str`$\times$ faster | -| **Power consumption** | ~`{python} cpu_power_mw_str` mW | ~`{python} edgetpu_power_mw_str` mW | — | ~`{python} edgetpu_power_ratio_str`$\times$ higher | -| **Energy per inference** | ~`{python} cpu_energy_mj_str` mJ | ~`{python} edgetpu_energy_mj_str` mJ | — | ~`{python} energy_ratio_str`$\times$ more efficient | +| **Metric** | **CPU (Cortex-M7)** | **EdgeTPU** | **Claimed** | **Validated?** | +|:-------------------------|----------------------------------:|--------------------------------------:|:-------------------------------------------------|-----------------------------------------------------:| +| **Inference latency** | ~`{python} cpu_latency_ms_str` ms | ~`{python} edgetpu_latency_ms_str` ms | `{python} inference_speedup_str` $\times$ faster | ✓ | +| **End-to-end latency** | ~`{python} cpu_e2e_ms_str` ms | ~`{python} edgetpu_e2e_ms_str` ms | — | ~`{python} e2e_speedup_str` $\times$ faster | +| **Power consumption** | ~`{python} cpu_power_mw_str` mW | ~`{python} edgetpu_power_mw_str` mW | — | ~`{python} edgetpu_power_ratio_str` $\times$ higher | +| **Energy per inference** | ~`{python} cpu_energy_mj_str` mJ | ~`{python} edgetpu_energy_mj_str` mJ | — | ~`{python} energy_ratio_str` $\times$ more efficient | -**What this reveals**: The `{python} inference_speedup_str`$\times$ inference speedup is real, but end-to-end improvement is only ~`{python} e2e_speedup_str`$\times$ because preprocessing (image capture, resize, normalize) runs on the CPU in both cases. EdgeTPU consumes more power but completes faster, yielding better energy efficiency per inference. +**What this reveals**: The `{python} inference_speedup_str` $\times$ inference speedup is real, but end-to-end improvement is only ~`{python} e2e_speedup_str` $\times$ because preprocessing (image capture, resize, normalize) runs on the CPU in both cases. EdgeTPU consumes more power but completes faster, yielding better energy efficiency per inference. **The deployment decision**: For battery-powered devices running infrequently (doorbell camera: ~100 inferences/day), CPU is more power-efficient overall because the device spends most time in sleep mode. For continuous operation (real-time video analytics: 30 FPS), EdgeTPU's per-inference energy efficiency dominates. @@ -2802,7 +2807,7 @@ Support infrastructure, particularly cooling systems, is a major component of to \index{Frequency Scaling!cubic power relationship} The relationship between computational performance and energy efficiency is a central tradeoff in modern ML system design. As systems push for higher performance, they often encounter diminishing returns in energy efficiency due to physical limitations in semiconductor scaling and power delivery [@koomey2011web]. This relationship is particularly evident in processor frequency scaling, where increasing clock frequency by 20% typically yields only modest performance improvements (around 5%) while dramatically increasing power consumption by up to 50%, reflecting the cubic relationship between voltage, frequency, and power consumption [@le2010dynamic]. -In deployment scenarios with strict energy constraints, particularly battery-powered edge devices and mobile applications, optimizing this performance-energy tradeoff becomes essential for practical viability. Model optimization techniques offer promising approaches to achieve better efficiency without material accuracy degradation. Numerical precision optimization techniques, which reduce computational requirements while maintaining model quality, demonstrate this tradeoff effectively. Research shows that reduced-precision computation can maintain model accuracy within 1–2% of the original while delivering 3–4$\times$ improvements in both inference speed and energy efficiency. +In deployment scenarios with strict energy constraints, particularly battery-powered edge devices and mobile applications, optimizing this performance-energy tradeoff becomes essential for practical viability. Model optimization techniques offer promising approaches to achieve better efficiency without material accuracy degradation. Numerical precision optimization techniques, which reduce computational requirements while maintaining model quality, demonstrate this tradeoff effectively. Research shows that reduced-precision computation can maintain model accuracy within 1–2% of the original while delivering 3–4 $\times$ improvements in both inference speed and energy efficiency. These optimization strategies span three interconnected dimensions: accuracy, computational performance, and energy efficiency. Advanced optimization methods enable fine-tuned control over this tradeoff space. Similarly, model optimization and compression techniques require careful balancing of accuracy losses against efficiency gains. The optimal operating point among these factors depends heavily on deployment requirements and constraints; mobile applications typically prioritize energy efficiency to extend battery life, while cloud-based services might optimize for accuracy even at higher power consumption costs, leveraging economies of scale and dedicated cooling infrastructure. @@ -2810,7 +2815,7 @@ Energy efficiency metrics now occupy a central position in AI system evaluation. ### Standardized Power Measurement {#sec-benchmarking-standardized-power-measurement-7fae} -Power measurement techniques like SPEC Power [@spec_power_website] have long served general computing [@lange2009identifying], but ML workloads expose a fundamental difficulty: instantaneous power consumption during a single inference can vary by 10$\times$ between the compute-intensive phases of matrix multiplication and the memory-stall phases of weight loading. A transformer attention layer may spike to 400 W while the subsequent data-movement phase drops to 40 W—all within a few milliseconds. This volatility means that any single-point measurement is misleading, and the act of measurement itself (instrumentation overhead, sampling-induced delays) can perturb the very power profile being characterized. +Power measurement techniques like SPEC Power [@spec_power_website] have long served general computing [@lange2009identifying], but ML workloads expose a fundamental difficulty: instantaneous power consumption during a single inference can vary by 10 $\times$ between the compute-intensive phases of matrix multiplication and the memory-stall phases of weight loading. A transformer attention layer may spike to 400 W while the subsequent data-movement phase drops to 40 W—all within a few milliseconds. This volatility means that any single-point measurement is misleading, and the act of measurement itself (instrumentation overhead, sampling-induced delays) can perturb the very power profile being characterized. The core challenge is therefore temporal: how do you characterize a quantity that fluctuates faster than most measurement instruments can sample? Dense matrix operations in transformer layers create short, intense power spikes requiring high-frequency sampling (>1 KHz) to capture accurately, while CNN inference tends toward more consistent power draw amenable to lower sampling rates. The measurement window must also account for ML-specific warm-up periods, where initial inferences consume more power due to cache population and pipeline initialization. Sliding-window averages over hundreds of inferences smooth these fluctuations into actionable efficiency numbers, but the window size itself becomes a design parameter that can hide or reveal different aspects of the power profile. @@ -2828,7 +2833,7 @@ The MLPerf Power methodology applies the standardized evaluation principles disc The benchmark has accumulated thousands of reproducible measurements submitted by industry organizations, demonstrating their latest hardware capabilities and the sector-wide focus on energy-efficient AI technology. Examine the three panels in @fig-power-trends to track how energy efficiency has evolved across system scales through successive MLPerf versions. The gains are not uniform—compare the datacenter panel against the tiny deployment panel to see where the most dramatic efficiency improvements have occurred and where progress has been more incremental. -::: {#fig-power-trends fig-env="figure" fig-pos="htb" fig-cap="**Energy Efficiency Gains**: Successive MLPerf inference benchmark versions show energy efficiency (samples per watt) improving up to 378$\times$ for datacenter workloads and 1070$\times$ for tinyML deployments across successive releases. Standardized measurement protocols enable meaningful cross-platform comparisons, driving sector-wide progress toward sustainable AI. Source: [@tschand2024mlperf]." fig-alt="Three line charts showing normalized energy efficiency across MLPerf versions: datacenter models up to 378× gain, edge models up to 4×, and tiny models up to 1070× improvement."} +::: {#fig-power-trends fig-env="figure" fig-pos="htb" fig-cap="**Energy Efficiency Gains**: Successive MLPerf inference benchmark versions show energy efficiency (samples per watt) improving up to 378 $\times$ for datacenter workloads and 1070 $\times$ for tinyML deployments across successive releases. Standardized measurement protocols enable meaningful cross-platform comparisons, driving sector-wide progress toward sustainable AI. Source: [@tschand2024mlperf]." fig-alt="Three line charts showing normalized energy efficiency across MLPerf versions: datacenter models up to 378 $\times$ gain, edge models up to 4 $\times$, and tiny models up to 1070 $\times$ improvement."} ```{.tikz} \begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}] %\node[anchor=south west]at(4.4,-5.54){% @@ -3200,7 +3205,7 @@ anchor=south,fill=white,inner sep=1pt]at (axis description cs: 0.82,0.49) {Bench ``` ::: -Analysis of these MLPerf Power trends reveals two notable patterns. First, energy efficiency improvements for traditional ML workloads (ResNet, BERT, RNN-T) have plateaued after initial gains; the low-hanging fruit of optimization has been harvested. Second, generative AI applications show dramatic efficiency increases (378$\times$ for Llama2, 113$\times$ for GPTJ), reflecting rapid innovation as researchers optimize these newer, larger models. This dichotomy suggests that established workloads have reached optimization maturity while frontier models still offer substantial efficiency headroom, a pattern likely to repeat as each new model architecture matures. +Analysis of these MLPerf Power trends reveals two notable patterns. First, energy efficiency improvements for traditional ML workloads (ResNet, BERT, RNN-T) have plateaued after initial gains; the low-hanging fruit of optimization has been harvested. Second, generative AI applications show dramatic efficiency increases (378 $\times$ for Llama2, 113 $\times$ for GPTJ), reflecting rapid innovation as researchers optimize these newer, larger models. This dichotomy suggests that established workloads have reached optimization maturity while frontier models still offer substantial efficiency headroom, a pattern likely to repeat as each new model architecture matures. The measurement techniques examined above—from timing protocols to power instrumentation—provide the raw data for benchmarking. Yet raw data alone does not guarantee sound conclusions. Converting measurements into meaningful comparisons requires understanding the systematic sources of error, bias, and misalignment that can make even carefully collected benchmark numbers misleading. @@ -3657,6 +3662,7 @@ Perhaps most insidiously, aggregate metrics hide subgroup failures. A model achi For many deployment scenarios, *how confident* the model is matters as much as *what* it predicts. A well-calibrated[^fn-calibration] model's confidence scores correspond to actual correctness probability: when it says "90% confident," it should be correct 90% of the time. \index{Calibration!definition and etymology} + [^fn-calibration]: **Calibration**: From Latin "calibrare," derived from Arabic "qalib" (a mold or form for casting metal). The term originally described adjusting measuring instruments against known standards. In ML, calibration ensures predicted probabilities match empirical frequencies: a model predicting "80% confident" should be correct 80% of the time across many such predictions. The surveying metaphor recurs: just as instruments must be calibrated against reference points, model confidence must be validated against ground truth. Compression frequently degrades calibration even when preserving accuracy—a critical concern when validating the model compression techniques from @sec-model-compression. A quantized model might maintain 94% accuracy while becoming overconfident, predicting 90%+ confidence on examples it gets wrong. This occurs because quantization affects the softmax distribution's shape, compressing probability mass toward the top prediction. Post-hoc calibration techniques (temperature scaling, Platt scaling) can partially correct this, but only if calibration is measured. @@ -3765,7 +3771,7 @@ The INT8 model's ECE of 0.089 indicates borderline calibration: confidence score **The fix**: Apply temperature scaling post-hoc to restore calibration. Temperature scaling learns a single scalar $T$ to divide logits before softmax: $\text{softmax}(z_i / T)$. Typical values: $T = 1.5$–$2.5$ for quantized models. In parallel, add edge-case examples to the test set to monitor that specific failure mode continuously. ::: -The Lottery Ticket Hypothesis (@sec-model-compression-lottery-ticket-hypothesis-1b3d) provides concrete benchmarking data illustrating what Pareto-efficient compression looks like. Through iterative pruning, researchers discovered that sparse subnetworks ("winning tickets") can match dense model performance: ResNet-18 subnetworks at 10–20% of original size achieve 93.2% accuracy versus 94.1% for the full model on CIFAR-10—a 0.9 percentage point drop for 80–90% size reduction [@frankle2019lottery]. BERT-base winning tickets retain 97% of original performance with 90% fewer parameters, requiring 5–8$\times$ less training time to converge. +The Lottery Ticket Hypothesis (@sec-model-compression-lottery-ticket-hypothesis-1b3d) provides concrete benchmarking data illustrating what Pareto-efficient compression looks like. Through iterative pruning, researchers discovered that sparse subnetworks ("winning tickets") can match dense model performance: ResNet-18 subnetworks at 10–20% of original size achieve 93.2% accuracy versus 94.1% for the full model on CIFAR-10—a 0.9 percentage point drop for 80–90% size reduction [@frankle2019lottery]. BERT-base winning tickets retain 97% of original performance with 90% fewer parameters, requiring 5–8 $\times$ less training time to converge. These numbers reveal the shape of compression trade-offs: the ResNet result shows diminishing returns (the last 80% of parameters contribute only 0.9% accuracy), while BERT demonstrates that aggressive pruning can preserve nearly all capability for the right architecture. Compression validation should establish similar trade-off curves for your specific model and task, identifying where your model sits on the Pareto frontier and whether further compression yields meaningful efficiency gains or merely degrades quality. @@ -3788,6 +3794,7 @@ Generation-specific metrics capture properties absent from discriminative benchm - **Perplexity**[^fn-perplexity] measures how well a model predicts held-out text (lower is better). A perplexity of 10 means the model is "10-way confused" on average. Useful for comparing models on the same corpus, but does not directly measure generation quality. \index{Perplexity!etymology} + [^fn-perplexity]: **Perplexity**: From Latin "perplexus" (entangled, confused), entering English in the 15th century to describe mental bewilderment. In information theory, Claude Shannon adapted the concept in the 1940s as 2^H(p) where H is entropy, measuring how "confused" or uncertain a probability distribution is. For language models, perplexity quantifies how surprised the model is by test text. A perplexity of 1 means perfect prediction; 100 means the model is as confused as randomly choosing among 100 equally likely words. \index{First-Token Latency!LLM responsiveness} @@ -4031,7 +4038,7 @@ anchor=north,fill=BackColor]{\textbf{Model-centric AI}}; \index{DataComp!data-centric benchmark} Data quality's primacy in AI development reflects an important shift in understanding that challenges the "more data is always better" assumption: *better* datasets, not just *larger* ones, produce more reliable and generalizable AI systems. Initiatives like DataPerf and DataComp[^fn-datacomp] have emerged to systematically evaluate how dataset improvements affect model performance. For instance, DataComp [@gadre2024datacomp] demonstrated that models trained on a carefully curated 30% subset of data achieved better results than those trained on the complete dataset, challenging the assumption that more data automatically leads to better performance [@northcutt2021pervasive]. -[^fn-datacomp]: **DataComp**: A benchmark for studying data-centric approaches to multimodal learning, introduced in 2023 by researchers from multiple institutions. DataComp provides a standardized framework where participants compete to create the best training dataset for CLIP-style models while keeping the model architecture and training code fixed. Results demonstrated that careful data filtering can yield models matching or exceeding those trained on 10$\times$ larger unfiltered datasets, quantifying data quality's impact on system performance. +[^fn-datacomp]: **DataComp**: A benchmark for studying data-centric approaches to multimodal learning, introduced in 2023 by researchers from multiple institutions. DataComp provides a standardized framework where participants compete to create the best training dataset for CLIP-style models while keeping the model architecture and training code fixed. Results demonstrated that careful data filtering can yield models matching or exceeding those trained on 10 $\times$ larger unfiltered datasets, quantifying data quality's impact on system performance. A persistent challenge in data benchmarking emerges from dataset saturation. When models achieve near-perfect accuracy on benchmarks like ImageNet, practitioners must distinguish whether performance gains represent genuine advances in AI capability or merely optimization to existing test sets. Look at the timeline in @fig-dataset-saturation and note the crossing points: one by one, AI systems surpassed human performance—first in handwriting recognition in the early 2000s, then speech and image recognition, and finally reading comprehension and language understanding by 2020. Each crossing renders the corresponding benchmark less useful as a differentiator. @@ -4348,7 +4355,7 @@ Benchmarking creates false confidence when standardized measurement obscures dep \index{Benchmarking!fallacy of direct performance translation} **Fallacy:** *Benchmark performance directly translates to real-world application performance.* -The seductive clarity of benchmark rankings leads teams to select systems as though leaderboard position predicts production behavior. It rarely does. As @sec-benchmarking-ml-measurement-challenges-60ea demonstrates, ML systems exhibit inherent variability from data quality issues, distribution shifts, and resource constraints absent in controlled evaluation. A language model achieving `{python} benchmark_accuracy_pct_str`% benchmark accuracy drops to `{python} production_accuracy_range_str`% accuracy in production when processing user-generated text with spelling errors, informal language, and domain-specific terminology. An inference system with `{python} benchmark_latency_mean_ms_str` ms mean latency on MLPerf experiences `{python} production_p99_range_str` ms p99 latency in production (`{python} latency_multiplier_str`$\times$ degradation) due to concurrent load, garbage collection pauses, and network variability. Teams relying solely on benchmark rankings systematically underestimate deployment complexity, leading to failed launches and costly re-engineering. +The seductive clarity of benchmark rankings leads teams to select systems as though leaderboard position predicts production behavior. It rarely does. As @sec-benchmarking-ml-measurement-challenges-60ea demonstrates, ML systems exhibit inherent variability from data quality issues, distribution shifts, and resource constraints absent in controlled evaluation. A language model achieving `{python} benchmark_accuracy_pct_str`% benchmark accuracy drops to `{python} production_accuracy_range_str`% accuracy in production when processing user-generated text with spelling errors, informal language, and domain-specific terminology. An inference system with `{python} benchmark_latency_mean_ms_str` ms mean latency on MLPerf experiences `{python} production_p99_range_str` ms p99 latency in production (`{python} latency_multiplier_str` $\times$ degradation) due to concurrent load, garbage collection pauses, and network variability. Teams relying solely on benchmark rankings systematically underestimate deployment complexity, leading to failed launches and costly re-engineering. **Pitfall:** *Optimizing exclusively for benchmark metrics without considering broader system requirements.* @@ -4357,12 +4364,12 @@ Benchmark leaderboards incentivize aggressive optimization, but the optimization \index{Single-Metric Evaluation!fallacy} **Fallacy:** *Single-metric evaluation provides sufficient insight into system performance.* -A single number is seductively simple: this system is "94% accurate" or "1,200 QPS fast." But production success requires balancing multiple competing objectives that any single metric obscures. As established in @sec-benchmarking-inference-metrics-78d4, modern inference systems demand evaluation across accuracy, latency, throughput, energy, and robustness dimensions. A recommendation model achieving `{python} rec_accuracy_pct_str`% accuracy with `{python} rec_p99_latency_ms_str` ms p99 latency fails service-level objectives requiring p99 < `{python} slo_p99_requirement_ms_str` ms despite excellent accuracy. Conversely, a system optimized for `{python} high_throughput_qps_str` QPS throughput achieves this rate while consuming `{python} high_power_w_str` W versus `{python} low_power_w_str` W for a slightly slower system at `{python} low_throughput_qps_str` QPS (`{python} power_ratio_str`$\times$ power difference). For battery-powered edge devices, the `{python} throughput_loss_pct_str`% throughput loss enables `{python} battery_multiplier_str`$\times$ longer operation time. Different stakeholders prioritize different metrics: ML engineers focus on accuracy, infrastructure teams on throughput and cost, product managers on latency percentiles. Single-metric optimization systematically produces systems that excel on one dimension while failing deployment requirements on others. +A single number is seductively simple: this system is "94% accurate" or "1,200 QPS fast." But production success requires balancing multiple competing objectives that any single metric obscures. As established in @sec-benchmarking-inference-metrics-78d4, modern inference systems demand evaluation across accuracy, latency, throughput, energy, and robustness dimensions. A recommendation model achieving `{python} rec_accuracy_pct_str`% accuracy with `{python} rec_p99_latency_ms_str` ms p99 latency fails service-level objectives requiring p99 < `{python} slo_p99_requirement_ms_str` ms despite excellent accuracy. Conversely, a system optimized for `{python} high_throughput_qps_str` QPS throughput achieves this rate while consuming `{python} high_power_w_str` W versus `{python} low_power_w_str` W for a slightly slower system at `{python} low_throughput_qps_str` QPS (`{python} power_ratio_str` $\times$ power difference). For battery-powered edge devices, the `{python} throughput_loss_pct_str`% throughput loss enables `{python} battery_multiplier_str` $\times$ longer operation time. Different stakeholders prioritize different metrics: ML engineers focus on accuracy, infrastructure teams on throughput and cost, product managers on latency percentiles. Single-metric optimization systematically produces systems that excel on one dimension while failing deployment requirements on others. \index{Benchmark Saturation!outdated benchmarks pitfall} **Pitfall:** *Using outdated benchmarks that no longer reflect current challenges and requirements.* -Benchmarks have inertia: teams continue reporting on established benchmarks long after those benchmarks cease to provide meaningful discrimination. Saturation occurs when multiple approaches achieve near-identical performance, eliminating useful comparison. ImageNet top-5 classification error decreased from `{python} imagenet_error_2010_pct_str`% in 2010 to `{python} imagenet_error_2015_pct_str`% by 2015, with the competition ending in `{python} imagenet_competition_end_year_str` when `{python} imagenet_teams_above_95_str` of `{python} imagenet_total_teams_str` teams exceeded 95% accuracy; further optimization beyond this threshold provides marginal value for most applications. Similarly, MNIST achieves `{python} mnist_accuracy_pct_str`% accuracy with simple models, yet teams still report improvements at the third decimal place. As discussed in @sec-benchmarking-statistical-methodological-issues-7aa5, statistical confidence intervals around these measurements often exceed the claimed improvements. Changing deployment contexts compound the problem: benchmarks designed for server hardware become misleading for edge devices with `{python} edge_memory_constraint_x_str`$\times$ memory constraints and `{python} edge_power_constraint_x_str`$\times$ power budgets. Effective benchmarking requires retiring saturated benchmarks and developing evaluation frameworks matching current deployment realities. +Benchmarks have inertia: teams continue reporting on established benchmarks long after those benchmarks cease to provide meaningful discrimination. Saturation occurs when multiple approaches achieve near-identical performance, eliminating useful comparison. ImageNet top-5 classification error decreased from `{python} imagenet_error_2010_pct_str`% in 2010 to `{python} imagenet_error_2015_pct_str`% by 2015, with the competition ending in `{python} imagenet_competition_end_year_str` when `{python} imagenet_teams_above_95_str` of `{python} imagenet_total_teams_str` teams exceeded 95% accuracy; further optimization beyond this threshold provides marginal value for most applications. Similarly, MNIST achieves `{python} mnist_accuracy_pct_str`% accuracy with simple models, yet teams still report improvements at the third decimal place. As discussed in @sec-benchmarking-statistical-methodological-issues-7aa5, statistical confidence intervals around these measurements often exceed the claimed improvements. Changing deployment contexts compound the problem: benchmarks designed for server hardware become misleading for edge devices with `{python} edge_memory_constraint_x_str` $\times$ memory constraints and `{python} edge_power_constraint_x_str` $\times$ power budgets. Effective benchmarking requires retiring saturated benchmarks and developing evaluation frameworks matching current deployment realities. \index{Amdahl's Law!optimization ceiling for ML pipelines} **Pitfall:** *Applying research-oriented benchmarks to evaluate production system performance.* @@ -4381,8 +4388,8 @@ The validation sequence reflects how problems manifest: hardware issues surface * **Benchmarks are proxies, not truth**: Standardized results like MLPerf provide comparative baselines, but production performance depends on your specific data distribution, load patterns, and SLA constraints. * **Granularity determines what you can diagnose**: Micro-benchmarks pinpoint which kernel is slow but miss system bottlenecks; end-to-end benchmarks capture production behavior but obscure root causes. Effective evaluation combines all three levels. * **The tail determines the user experience**: Average latency obscures performance failures. Benchmarking for interactive systems must report p95 and p99 tail latencies to ensure SLO compliance under load. -* **Amdahl's Law sets the optimization ceiling**: Model speedup is limited by the non-model fraction of the pipeline. If preprocessing consumes 50% of the latency, even an infinite-speed model can only achieve 2$\times$ total system improvement. -* **Precision is an energy lever**: INT8 quantization provides 4$\times$ memory reduction but can deliver 10–20$\times$ energy reduction by shifting the balance from energy-intensive DRAM access to efficient integer arithmetic. +* **Amdahl's Law sets the optimization ceiling**: Model speedup is limited by the non-model fraction of the pipeline. If preprocessing consumes 50% of the latency, even an infinite-speed model can only achieve 2 $\times$ total system improvement. +* **Precision is an energy lever**: INT8 quantization provides 4 $\times$ memory reduction but can deliver 10–20 $\times$ energy reduction by shifting the balance from energy-intensive DRAM access to efficient integer arithmetic. * **Compression validation requires more than accuracy**: INT8 quantization may preserve top-line accuracy while degrading calibration and edge-case robustness—failures invisible to aggregate metrics but critical for deployment. ::: diff --git a/book/quarto/contents/vol1/conclusion/conclusion.qmd b/book/quarto/contents/vol1/conclusion/conclusion.qmd index 1b552e16f8..c4156f5b4d 100644 --- a/book/quarto/contents/vol1/conclusion/conclusion.qmd +++ b/book/quarto/contents/vol1/conclusion/conclusion.qmd @@ -191,9 +191,9 @@ Together, these five workloads span the full deployment spectrum from datacenter | **Journey Phase** | **System Lens** | **MobileNetV2 Implementation** | |:----------------------------------------------|:-------------------------------|:---------------------------------------------------------------------------------------------------------------| | **Foundations (@sec-introduction)** | The AI Triad | Bounded by **Machine** constraints (Battery/Thermal) | -| **Architecture (@sec-network-architectures)** | Algorithmic Efficiency | **Depthwise Separable Convolutions**: 8–9$\times$ reduction in FLOPs vs ResNet-50 | +| **Architecture (@sec-network-architectures)** | Algorithmic Efficiency | **Depthwise Separable Convolutions**: 8–9 $\times$ reduction in FLOPs vs ResNet-50 | | **Training (@sec-model-training)** | Throughput vs Latency | Optimized for **Single-Stream** throughput; training requires data augmentation for robustness | -| **Compression (@sec-model-compression)** | Navigating the Pareto Frontier | **INT8 Quantization**: 4$\times$ memory reduction with minimal accuracy loss (<1%) | +| **Compression (@sec-model-compression)** | Navigating the Pareto Frontier | **INT8 Quantization**: 4 $\times$ memory reduction with minimal accuracy loss (<1%) | | **Acceleration (@sec-hardware-acceleration)** | Honoring the Silicon Contract | Mapping kernels to **Mobile NPUs** (e.g., Apple Neural Engine) to maximize hardware utilization | | **Serving (@sec-model-serving)** | Respecting the Latency Budget | **P99 < 50 ms** constraint; optimizing preprocessing (resize/normalize) to avoid CPU bottlenecks | | **Operations (@sec-ml-operations)** | Managing System Entropy | **Drift Monitoring**: Detecting accuracy decay across heterogeneous device populations and lighting conditions | @@ -215,7 +215,7 @@ Throughout this book, each Part introduced quantitative principles that govern M | 4 | Silicon Contract | II: Build | Every architecture bets on which hardware resource it saturates | Mismatched hardware wastes money; matched hardware achieves peak throughput | | 5 | Pareto Frontier | III: Optimize | Multi-objective optimization; no free improvements | There is no universal optimum; every gain trades against another metric | | 6 | Arithmetic Intensity Law | III: Optimize | $R = \min(R_{peak},\; I \times BW)$ | Adding compute to a memory-bound model yields zero gain | -| 7 | Energy-Movement Invariant | III: Optimize | $E_{move} \gg E_{compute}$ (100–1,000$\times$) | Data locality, not raw FLOPS, drives efficiency | +| 7 | Energy-Movement Invariant | III: Optimize | $E_{move} \gg E_{compute}$ (100–1,000 $\times$) | Data locality, not raw FLOPS, drives efficiency | | 8 | Amdahl's Law | III: Optimize | $\text{Speedup} = \frac{1}{(1-p) + \frac{p}{s}}$ | The serial fraction caps all parallelism gains | | 9 | Verification Gap | IV: Deploy | $P(f(X) \approx Y) > 1 - \epsilon$ | ML testing is statistical; you bound error, not prove correctness | | 10 | Statistical Drift Invariant | IV: Deploy | $\text{Acc}(t) \approx \text{Acc}_0 - \lambda \cdot D(P_t \Vert P_0)$ | Models decay without code changes; the world drifts away from training data | @@ -325,7 +325,7 @@ We can apply the **Iron Law** (Invariant 3) and **Arithmetic Intensity** (Invari **The Systems Insight:** -The memory time $T_{mem}$ is `{python} ratio_str`$\times$ larger than compute time $T_{comp}$. The system is heavily memory-bound (arithmetic intensity $\approx$ 1). To honor the **Silicon Contract**, we must either increase **Arithmetic Intensity** (via batching users to reuse $D_{vol}$) or reduce Data Volume (via quantization to INT4). A systems engineer who optimizes compute kernels ($T_{comp}$) without addressing memory ($T_{mem}$) wastes 100% of their effort. +The memory time $T_{mem}$ is `{python} ratio_str` $\times$ larger than compute time $T_{comp}$. The system is heavily memory-bound (arithmetic intensity $\approx$ 1). To honor the **Silicon Contract**, we must either increase **Arithmetic Intensity** (via batching users to reuse $D_{vol}$) or reduce Data Volume (via quantization to INT4). A systems engineer who optimizes compute kernels ($T_{comp}$) without addressing memory ($T_{mem}$) wastes 100% of their effort. ::: The Cost of a Token calculation illustrates a broader truth: the invariant framework is not an abstract taxonomy but a diagnostic instrument. Every chapter in this book applied these invariants to specific engineering decisions, often without naming them explicitly. The following section traces those applications across the three domains where they mattered most—building foundations, engineering for scale, and navigating production reality—to show how the framework we have just formalized has already been guiding our thinking throughout this book. @@ -406,13 +406,13 @@ As ML systems move beyond research labs, four deployment paradigms test differen Cloud deployment prioritizes throughput and scalability, the regime where ResNet-50 and DLRM operate, achieving high GPU utilization through kernel fusion, mixed precision training, and gradient compression. @sec-model-compression and @sec-model-training explored these techniques, demonstrating how they combine to balance performance optimization with cost efficiency at scale. \index{AI Democratization!edge deployment} -In contrast, mobile and edge systems face stringent power, memory, and latency constraints that demand sophisticated hardware-software co-design. Efficient architectures introduced in @sec-network-architectures (such as depthwise separable convolutions and neural architecture search) combined with compression techniques from @sec-model-compression (such as quantization and pruning) enable deployment on devices with 100–1,000$\times$ less computational power than datacenters. Systems that cannot run on billions of edge devices cannot achieve global impact, making edge deployment essential for AI democratization[^fn-ai-democratization]. +In contrast, mobile and edge systems face stringent power, memory, and latency constraints that demand sophisticated hardware-software co-design. Efficient architectures introduced in @sec-network-architectures (such as depthwise separable convolutions and neural architecture search) combined with compression techniques from @sec-model-compression (such as quantization and pruning) enable deployment on devices with 100–1,000 $\times$ less computational power than datacenters. Systems that cannot run on billions of edge devices cannot achieve global impact, making edge deployment essential for AI democratization[^fn-ai-democratization]. [^fn-ai-democratization]: **AI Democratization**: Making AI accessible beyond a small number of well-resourced organizations through efficient systems engineering. Mobile-optimized models and cloud APIs can widen access, but doing so sustainably requires systematic optimization across hardware, algorithms, and infrastructure to maintain quality at scale. Generative AI systems—the frontier that GPT-2/Llama exposed—apply the principles at unprecedented scale. Autoregressive generation is inherently memory-bound (each token requires loading the full model weights), making the **Arithmetic Intensity Law** the governing constraint. Novel techniques like dynamic model partitioning and speculative decoding[^fn-speculative-decoding] reshape the **Silicon Contract** by trading compute for latency, demonstrating how our principles adapt even as technologies push infrastructure boundaries. -[^fn-speculative-decoding]: **Speculative Decoding**: Inference optimization where a smaller draft model generates candidate tokens that a larger target model verifies in parallel. Since autoregressive generation is memory-bound (each token requires loading the full model), speculative decoding trades compute for latency: the draft model proposes 4–8 tokens; the target verifies them in a single forward pass. Achieves 2–3$\times$ speedup when draft acceptance rates exceed 70%, making it essential for interactive LLM applications. +[^fn-speculative-decoding]: **Speculative Decoding**: Inference optimization where a smaller draft model generates candidate tokens that a larger target model verifies in parallel. Since autoregressive generation is memory-bound (each token requires loading the full model), speculative decoding trades compute for latency: the draft model proposes 4–8 tokens; the target verifies them in a single forward pass. Achieves 2–3 $\times$ speedup when draft acceptance rates exceed 70%, making it essential for interactive LLM applications. At the opposite extreme, TinyML and embedded systems, the domain of our KWS/Wake Vision Lighthouse, face kilobyte memory budgets, milliwatt power envelopes, and decade-long deployment lifecycles. Success in these contexts validates the full systems engineering approach: careful measurement reveals actual bottlenecks, hardware co-design maximizes efficiency, and planning for failure ensures reliability despite severe resource limitations. Mobile deployment constraints have driven breakthrough techniques like MobileNets and EfficientNets that benefit all AI deployment contexts, demonstrating how systems constraints catalyze algorithmic innovation. diff --git a/book/quarto/contents/vol1/data_engineering/data_engineering.qmd b/book/quarto/contents/vol1/data_engineering/data_engineering.qmd index 9cacf18896..f8e466f1af 100644 --- a/book/quarto/contents/vol1/data_engineering/data_engineering.qmd +++ b/book/quarto/contents/vol1/data_engineering/data_engineering.qmd @@ -360,14 +360,14 @@ $$ \text{Data Selection Gain} \propto \frac{\text{Information Entropy}}{\text{Da Underlying these physical properties is a fundamental constraint we call the *energy-movement invariant*: moving data always dominates the energy budget. ::: {.callout-perspective title="The Energy-Movement Invariant"} -A corollary of the **Iron Law** (@sec-introduction-iron-law-ml-systems-c32a) for data engineering is that **moving a bit costs 100–1,000$\times$ more energy than computing on it.** While @sec-model-compression examines the energy cost inside the processor, we must also consider the cost of the information flow from the external world. The following table quantifies just how dramatic these differences are: +A corollary of the **Iron Law** (@sec-introduction-iron-law-ml-systems-c32a) for data engineering is that **moving a bit costs 100–1,000 $\times$ more energy than computing on it.** While @sec-model-compression examines the energy cost inside the processor, we must also consider the cost of the information flow from the external world. The following table quantifies just how dramatic these differences are: -| **Operation** | **Energy (pJ)** | **Relative Cost** | -|:-----------------------------------|-----------------------------------:|--------------------------------------------:| -| **32-bit Floating Point MAC** | `{python} energy_flop_fp32_str` pJ | 1$\times$ | -| **DRAM Memory Access (32-bit)** | **`{python} energy_dram_str` pJ** | **`{python} dram_multiplier_str`$\times$** | -| **Local SSD Access (per bit)** | ~10,000 pJ | `{python} local_ssd_multiplier_str`$\times$ | -| **Network Transfer (Data Center)** | ~50,000 pJ | `{python} network_multiplier_str`$\times$ | +| **Operation** | **Energy (pJ)** | **Relative Cost** | +|:-----------------------------------|-----------------------------------:|---------------------------------------------:| +| **32-bit Floating Point MAC** | `{python} energy_flop_fp32_str` pJ | 1 $\times$ | +| **DRAM Memory Access (32-bit)** | **`{python} energy_dram_str` pJ** | **`{python} dram_multiplier_str` $\times$** | +| **Local SSD Access (per bit)** | ~10,000 pJ | `{python} local_ssd_multiplier_str` $\times$ | +| **Network Transfer (Data Center)** | ~50,000 pJ | `{python} network_multiplier_str` $\times$ | **Systems Implication**: Data has physical mass. If you prune 50% of your training data through deduplication, you are not just saving disk space; you are eliminating the most energy-intensive stages of the training lifecycle. This is *why* data selection is the highest-leverage tool in the systems engineer's toolkit: it addresses the problem at the most expensive source. ::: @@ -990,14 +990,14 @@ Data quality and diversity ultimately determine whether these constraints can be These requirements create a multi-dimensional design space where data engineering choices cascade through system performance. @tbl-kws-design-space quantifies key trade-offs, enabling principled decisions rather than ad-hoc selection. -| **Design Choice** | **Quality Impact** | **Latency Impact** | **Cost Impact** | **Memory Impact** | -|:-----------------------------------|---------------------:|:--------------------------|--------------------------:|:-------------------------| -| **16kHz vs 8kHz sampling** | +2–4% accuracy | 2$\times$ storage | 2$\times$ processing | 2$\times$ feature size | -| **13 vs 40 MFCC coefficients** | +3–5% accuracy | 3$\times$ feature compute | Minimal | 3$\times$ feature memory | -| **1M vs 10M training examples** | +5–8% accuracy | 10$\times$ training time | 10$\times$ labeling cost | 10$\times$ storage | -| **Clean vs noisy training data** | +10–15% real-world | Minimal | 3$\times$ collection cost | Minimal | -| **Local vs cloud inference** | −2% accuracy (quant) | 10ms vs 100ms | $0 vs $0.001/query | 16 KB vs unlimited | -| **Synthetic vs real augmentation** | +3–5% robustness | Minimal | 10$\times$ cheaper | Minimal | +| **Design Choice** | **Quality Impact** | **Latency Impact** | **Cost Impact** | **Memory Impact** | +|:-----------------------------------|---------------------:|:---------------------------|---------------------------:|:--------------------------| +| **16kHz vs 8kHz sampling** | +2–4% accuracy | 2 $\times$ storage | 2 $\times$ processing | 2 $\times$ feature size | +| **13 vs 40 MFCC coefficients** | +3–5% accuracy | 3 $\times$ feature compute | Minimal | 3 $\times$ feature memory | +| **1M vs 10M training examples** | +5–8% accuracy | 10 $\times$ training time | 10 $\times$ labeling cost | 10 $\times$ storage | +| **Clean vs noisy training data** | +10–15% real-world | Minimal | 3 $\times$ collection cost | Minimal | +| **Local vs cloud inference** | −2% accuracy (quant) | 10ms vs 100ms | \$0 vs \$0.001/query | 16 KB vs unlimited | +| **Synthetic vs real augmentation** | +3–5% robustness | Minimal | 10 $\times$ cheaper | Minimal | : **KWS Data Engineering Design Space**: Each design choice creates quantifiable trade-offs across the four pillars. Higher sampling rates improve quality but double storage and processing (scalability impact). More training data improves accuracy but multiplies labeling costs (governance/cost impact). Local inference eliminates latency but requires aggressive quantization (quality/reliability trade-off). This design space analysis guides systematic optimization rather than intuition-based decisions. {#tbl-kws-design-space} @@ -1059,7 +1059,7 @@ labeled_k = f"{labeled_examples_value/1000:.0f}" From @tbl-kws-design-space, the 64 KB memory limit eliminates: -- 40 MFCC coefficients (3$\times$ memory) → Must use 13 MFCCs +- 40 MFCC coefficients (3 $\times$ memory) → Must use 13 MFCCs - Cloud inference (requires network stack) → Must use local inference #### Step 2: Calculate Budget Allocation {.unnumbered} @@ -1084,20 +1084,20 @@ Current accuracy budget: Options from design space: -- 16kHz sampling: +2–4% accuracy, 2$\times$ storage cost ✓ (fits budget) -- Noisy training data: +10–15% real-world accuracy, 3$\times$ collection cost -- Synthetic augmentation: +3–5% robustness, 10$\times$ cheaper than real data ✓ +- 16kHz sampling: +2–4% accuracy, 2 $\times$ storage cost ✓ (fits budget) +- Noisy training data: +10–15% real-world accuracy, 3 $\times$ collection cost +- Synthetic augmentation: +3–5% robustness, 10 $\times$ cheaper than real data ✓ #### Step 4: Final Configuration {.unnumbered} -| **Choice** | **Selection** | **Rationale** | -|:----------------------|:-------------------------|:---------------------------------------------------| -| **Sampling rate** | 16kHz | +3% accuracy worth 2$\times$ storage within budget | -| **MFCC coefficients** | 13 | Memory-constrained, non-negotiable | -| **Training examples** | 750K real + 2M synthetic | Budget-optimal mix | -| **Data diversity** | Noisy + clean mix | Critical for real-world deployment | -| **Inference** | Local, 8-bit quantized | Memory-constrained | -| **Augmentation** | Heavy synthetic | 10$\times$ cost efficiency | +| **Choice** | **Selection** | **Rationale** | +|:----------------------|:-------------------------|:----------------------------------------------------| +| **Sampling rate** | 16kHz | +3% accuracy worth 2 $\times$ storage within budget | +| **MFCC coefficients** | 13 | Memory-constrained, non-negotiable | +| **Training examples** | 750K real + 2M synthetic | Budget-optimal mix | +| **Data diversity** | Noisy + clean mix | Critical for real-world deployment | +| **Inference** | Local, 8-bit quantized | Memory-constrained | +| **Augmentation** | Heavy synthetic | 10 $\times$ cost efficiency | **Projected Outcome**: 97–99% accuracy (meeting target), $145K spend (under budget), 48 KB model (under limit). @@ -1131,15 +1131,15 @@ Just as systems engineers memorize latency numbers, ML engineers should internal **Time Constants** -| **Operation** | **Duration** | **Bottleneck** | -|:-----------------------------------|-------------:|:------------------------------------| -| **Label 1M images (crowdsourced)** | 2–4 weeks | Annotation throughput | -| **Train ResNet-50 on ImageNet** | 4–6 hours | Compute (8$\times$ A100, optimized) | -| **Feature store lookup** | 1–10 ms | Network + cache | +| **Operation** | **Duration** | **Bottleneck** | +|:-----------------------------------|-------------:|:-------------------------------------| +| **Label 1M images (crowdsourced)** | 2–4 weeks | Annotation throughput | +| **Train ResNet-50 on ImageNet** | 4–6 hours | Compute (8 $\times$ A100, optimized) | +| **Feature store lookup** | 1–10 ms | Network + cache | The contrast matters: **weeks** for human labeling, **hours** for GPU training, **milliseconds** for serving. Labeling is the bottleneck. -**The 1000$\times$ Rule**: Labeling typically costs 1,000–3,000$\times$ more than the compute to train on that data. A $100K labeling budget buys data that trains on $30–100 of GPU time. +**The 1000 $\times$ Rule**: Labeling typically costs 1,000–3,000 $\times$ more than the compute to train on that data. A $100K labeling budget buys data that trains on $30–100 of GPU time. **The 80/20 Split**: 80% of data engineering effort goes to 20% of features: the "long tail" of edge cases, rare categories, and quality exceptions. ::: @@ -2055,7 +2055,7 @@ batch_core_hours_str = fmt(batch_core_hours_value, precision=0, commas=False) 2. **Stream Requirements**: To sustain `{python} throughput_str` GB/s with <100ms latency, you need ~50 dedicated cores + redundant backups (always on). Cost: `{python} stream_cores_str` cores $\times$ `{python} stream_hours_str` hrs $\times$ USD `{python} stream_cost_per_hr_str`/hr = **USD `{python} stream_cost_str`/day**. 3. **Batch Requirements**: Process `{python} hourly_tb_str` TB (1 hour data) in 10 mins. High throughput (sequential I/O) is efficient. You need 200 cores for 10 mins/hour $\times$ 24 hours = `{python} batch_core_hours_str` core-hours/day. Cost: `{python} batch_core_hours_str` $\times$ USD `{python} stream_cost_per_hr_str` = **USD `{python} batch_cost_str`/day**. -**The Engineering Conclusion**: Real-time is **~`{python} cost_ratio_str`$\times$ more expensive** for the same data volume. Only pay this tax if the value of <1s latency justifies it. +**The Engineering Conclusion**: Real-time is **~`{python} cost_ratio_str` $\times$ more expensive** for the same data volume. Only pay this tax if the value of <1s latency justifies it. ::: #### ETL and ELT Comparison {#sec-data-engineering-etl-elt-comparison-2e2b} @@ -2372,7 +2372,7 @@ n_models_str = fmt(n_models_value, precision=0, commas=False) * Query compute: `{python} n_models_str` models $\times$ USD `{python} query_cost_per_str`/query $\times$ `{python} retention_days_str` days = USD `{python} elt_query_str`/month * Schema change cost: Rewrite SQL query (~30 minutes) per change -**The Engineering Conclusion**: ETL saves USD `{python} savings_str`/month in storage but costs 8$\times$ more engineering time per schema change. If your feature definitions change weekly, ELT's flexibility pays for itself. If schemas are stable, ETL's lower storage cost dominates. The break-even point: if you change schemas fewer than once per month, ETL wins on total cost. +**The Engineering Conclusion**: ETL saves USD `{python} savings_str`/month in storage but costs 8 $\times$ more engineering time per schema change. If your feature definitions change weekly, ELT's flexibility pays for itself. If schemas are stable, ETL's lower storage cost dominates. The break-even point: if you change schemas fewer than once per month, ETL wins on total cost. ::: When implementing streaming components within ETL/ELT architectures, distributed systems principles become critical. The CAP theorem\index{CAP Theorem!streaming systems}[^fn-cap-theorem]—which states that distributed systems cannot simultaneously guarantee Consistency (all nodes see the same data), Availability (the system remains operational), and Partition tolerance (the system continues despite network failures)—constrains streaming system design choices. Apache Kafka\index{Apache Kafka!event streaming}[^fn-apache-kafka] emphasizes consistency and partition tolerance, making it well-suited for reliable event ordering but potentially experiencing availability issues during network partitions. Apache Pulsar emphasizes availability and partition tolerance, providing better fault tolerance but with relaxed consistency guarantees. Amazon Kinesis balances all three properties through careful configuration but requires understanding these trade-offs for proper deployment. @@ -2449,7 +2449,7 @@ Outlier detection\index{Outlier Detection!data cleaning} and treatment is anothe Quality assessment complements data cleaning by systematically evaluating the reliability and usefulness of data across multiple dimensions: accuracy, completeness, consistency, and timeliness. In production systems, data quality degrades in subtle ways that basic metrics miss: fields that never contain nulls suddenly show sparse patterns, numeric distributions drift from their training ranges, or categorical values appear that were not present during model development. -To address these subtle degradation patterns, production quality monitoring requires specific metrics beyond simple missing value counts as discussed in @sec-data-engineering-quality-validation-monitoring-498f. Critical indicators include null value patterns by feature (sudden increases suggest upstream failures), count anomalies (10$\times$ increases often indicate data duplication or pipeline errors), value range violations (prices becoming negative, ages exceeding realistic bounds), and join failure rates between data sources. *Statistical drift* detection[^fn-data-drift] becomes essential by monitoring means, variances, and quantiles of features over time to catch gradual degradation before it impacts model performance. For example, in an e-commerce recommendation system, the average user session length might gradually increase from 8 minutes to 12 minutes over six months due to improved site design, but a sudden drop to 3 minutes suggests a data collection bug. +To address these subtle degradation patterns, production quality monitoring requires specific metrics beyond simple missing value counts as discussed in @sec-data-engineering-quality-validation-monitoring-498f. Critical indicators include null value patterns by feature (sudden increases suggest upstream failures), count anomalies (10 $\times$ increases often indicate data duplication or pipeline errors), value range violations (prices becoming negative, ages exceeding realistic bounds), and join failure rates between data sources. *Statistical drift* detection[^fn-data-drift] becomes essential by monitoring means, variances, and quantiles of features over time to catch gradual degradation before it impacts model performance. For example, in an e-commerce recommendation system, the average user session length might gradually increase from 8 minutes to 12 minutes over six months due to improved site design, but a sudden drop to 3 minutes suggests a data collection bug. [^fn-data-drift]: **Data Drift**: The term "drift" comes from Old Norse "drifa" (to drive or push), originally describing slow movement caused by external forces like wind or current. In ML, data drift describes the gradual movement of production data distributions away from training baselines, pushed by forces like changing user behavior or evolving systems. Unlike model decay (degradation from unchanged data), drift specifically denotes distribution shift requiring monitoring of feature statistics to detect before accuracy degrades. @@ -2459,7 +2459,7 @@ Transformation techniques\index{Data Transformation!techniques} convert data fro Beyond numerical scaling, other transformations might involve encoding categorical variables\index{Categorical Encoding!one-hot encoding}, handling date and time data, or creating derived features. For instance, one-hot encoding is often used to convert categorical variables into a format that can be readily understood by many machine learning algorithms. Categorical encodings must handle both the categories present during training and unknown categories encountered during serving. A reliable approach computes the category vocabulary during training (the set of all observed categories), persists it with the model, and during serving either maps unknown categories to a special "unknown" token or uses default values. Without this discipline, serving encounters categories the model never saw during training, potentially causing errors or degraded performance. -A health prediction model receives raw GPS coordinates for each patient visit, but latitude and longitude alone tell the model nothing about healthcare access. An engineer who understands the domain creates a new feature: *distance to nearest hospital*. Suddenly the model discovers that patients more than 30 miles from an emergency room have measurably worse outcomes for time-sensitive conditions—a pattern invisible in the raw coordinates. +A health prediction model receives raw GPS coordinates for each patient visit, but latitude and longitude alone tell the model nothing about healthcare access. An engineer who understands the domain creates a new feature: *distance to nearest hospital*. Suddenly the model discovers that patients more than 50 km from an emergency room have measurably worse outcomes for time-sensitive conditions—a pattern invisible in the raw coordinates. Feature engineering\index{Feature Engineering!definition}\index{Feature Engineering!domain knowledge} is this act of using domain knowledge to create new features that make machine learning algorithms work more effectively. The step is often considered more art than science, requiring creativity and deep understanding of both the data and the problem at hand. Feature engineering might involve combining existing features, extracting information from complex data types, or creating entirely new features based on domain insights. In a retail recommendation system, for example, engineers might create features that capture the recency, frequency, and monetary value of customer purchases, known as RFM analysis\index{RFM Analysis!feature engineering} [@kuhn2013applied]. @@ -2501,7 +2501,7 @@ Reliability in the KWS pipeline requires reproducible feature extraction. Audio These challenges manifest when quality assessment must keep pace with incoming data, when feature engineering requires computing statistics across entire datasets before transforming individual records, and when transformation pipelines create bottlenecks at massive volumes. Processing must scale from development (gigabytes on laptops) through production (terabytes across clusters) while maintaining consistent behavior. -To address these scaling bottlenecks, data must be partitioned across multiple computing resources, which introduces coordination challenges. Distributed coordination is constrained by network round-trip times: local operations complete in microseconds while network coordination requires milliseconds, creating a 1,000$\times$ latency difference. This constraint explains why operations requiring global coordination (like computing normalization statistics across 100 machines) create bottlenecks. Each partition computes local statistics quickly, but combining them requires information from all partitions. +To address these scaling bottlenecks, data must be partitioned across multiple computing resources, which introduces coordination challenges. Distributed coordination is constrained by network round-trip times: local operations complete in microseconds while network coordination requires milliseconds, creating a 1,000 $\times$ latency difference. This constraint explains why operations requiring global coordination (like computing normalization statistics across 100 machines) create bottlenecks. Each partition computes local statistics quickly, but combining them requires information from all partitions. Data locality becomes critical at this scale. At 10 GB/s peak throughput, transferring one terabyte of training data across a network takes on the order of 100 seconds; reading the same amount from a 5 GB/s SSD takes on the order of 200 seconds. These are the same order of magnitude, which drives ML system design toward compute-follows-data architectures. When processing nodes access local data at RAM speeds (50–200 GB/s) but must coordinate over networks limited to 1–10 GB/s, the bandwidth mismatch creates severe bottlenecks. Geographic distribution amplifies these challenges: cross-datacenter coordination must handle network latency (50–200 ms between regions), partial failures, and regulatory constraints preventing data from crossing borders. Understanding which operations parallelize easily versus those requiring expensive coordination determines system architecture and performance characteristics. This overhead constitutes a *coordination tax* that limits distributed data processing. @@ -2519,20 +2519,20 @@ Data locality becomes critical at this scale. At 10 GB/s peak throughput, transf - Each node computes local mean: **~0.01 seconds** (10GB at RAM speed) - Send 100 partial means (8 bytes each): **<1 ms** - Aggregate: **negligible** -- Total: **~0.01 seconds** (10,000$\times$ faster) +- Total: **~0.01 seconds** (10,000 $\times$ faster) **The Engineering Lesson**: Operations that *reduce* data (sum, mean, count) should always run locally first. Operations that *expand* data (joins, cross-products) face unavoidable network costs. Pipeline design should minimize data movement by pushing computation to where data resides, the compute-follows-data principle central to systems like MapReduce [@dean2004mapreduce], Spark [@zaharia2010spark], and modern ML frameworks. ::: Single-machine processing\index{Single-Machine Processing!scalability} suffices for surprisingly large workloads when engineered carefully. Modern servers with 256 gigabytes RAM can process datasets of several terabytes using out-of-core processing that streams data from disk. Libraries like Dask\index{Dask!out-of-core processing} or Vaex\index{Lazy Evaluation!data processing} enable pandas-like APIs that automatically stream and parallelize computations across multiple cores. Before investing in distributed processing infrastructure, teams should exhaust single-machine optimization: using efficient data formats (Parquet[^fn-parquet] instead of CSV), minimizing memory allocations, leveraging vectorized operations, and exploiting multi-core parallelism. The operational simplicity of single-machine processing—no network coordination, no partial failures, simple debugging—makes it preferable when performance is adequate. -[^fn-parquet]: **Parquet**: Named after the herringbone wood flooring pattern, this columnar storage format (developed by Cloudera and Twitter, 2013) stores data in nested column structures that visually resemble parquet flooring tiles. The name reflects how data is interlocked column-by-column rather than row-by-row. For ML systems, this columnar layout enables reading only required features and achieves 5–10$\times$ I/O reduction compared to row-based formats like CSV. +[^fn-parquet]: **Parquet**: Named after the herringbone wood flooring pattern, this columnar storage format (developed by Cloudera and Twitter, 2013) stores data in nested column structures that visually resemble parquet flooring tiles. The name reflects how data is interlocked column-by-column rather than row-by-row. For ML systems, this columnar layout enables reading only required features and achieves 5–10 $\times$ I/O reduction compared to row-based formats like CSV. Distributed processing frameworks become necessary when data volumes or computational requirements exceed single-machine capacity, but the speedup achievable through parallelization faces inherent limits described by **Amdahl's Law**. Let $S$ be the serial fraction, $p$ the parallelizable fraction, and $N$ the number of workers. @eq-amdahl-data gives the bound: $$\text{Speedup} \leq \frac{1}{S + \frac{p}{N}}$$ {#eq-amdahl-data} -where $S$ represents the serial fraction of work that cannot parallelize, $p$ the parallel fraction (with $S + p = 1$), and $N$ the number of processors. This explains why distributing our KWS feature extraction across 64 cores achieves only a 64$\times$ speedup when the work is embarrassingly parallel ($S \approx 0$), but coordination-heavy operations like computing global normalization statistics might achieve only 10$\times$ speedup even with 64 cores due to the serial aggregation phase. Understanding this relationship guides architectural decisions: operations with high serial fractions should run on fewer, faster cores rather than many slower cores, while highly parallel workloads benefit from maximum distribution. @sec-model-training examines distributed training architectures that apply these principles at cluster scale. +where $S$ represents the serial fraction of work that cannot parallelize, $p$ the parallel fraction (with $S + p = 1$), and $N$ the number of processors. This explains why distributing our KWS feature extraction across 64 cores achieves only a 64 $\times$ speedup when the work is embarrassingly parallel ($S \approx 0$), but coordination-heavy operations like computing global normalization statistics might achieve only 10 $\times$ speedup even with 64 cores due to the serial aggregation phase. Understanding this relationship guides architectural decisions: operations with high serial fractions should run on fewer, faster cores rather than many slower cores, while highly parallel workloads benefit from maximum distribution. @sec-model-training examines distributed training architectures that apply these principles at cluster scale. Apache Spark\index{Apache Spark!distributed processing} provides a distributed computing framework that parallelizes transformations across clusters of machines, handling data partitioning, task scheduling, and fault tolerance automatically. Beam provides a unified API for both batch and streaming processing, enabling the same transformation logic to run on multiple execution engines (Spark, Flink, Dataflow). TensorFlow's tf.data API optimizes data loading pipelines for ML training, supporting distributed reading, prefetching, and transformation. The choice of framework depends on whether processing is batch or streaming, how transformations parallelize, and what execution environment is available. @@ -2562,7 +2562,7 @@ With raw inputs cleaned, normalized, and transformed into usable features, the r [^fn-ground-truth-etymology]: **Ground Truth**: Originally a term from meteorology and remote sensing. When a satellite estimates crop yields or weather from orbit, scientists would send a team to the physical location—the "ground"—to verify the "truth." In ML, it reminds us that our labels are merely the best available proxy for reality, verified by direct human observation. -Unlike automated transformations that can be parallelized across machines, labeling introduces human judgment into the pipeline, creating unique engineering challenges. A crowdsourced annotator might mislabel a whispered "Alexa" as background noise. An expert radiologist might disagree with a colleague about a borderline diagnosis. These disagreements are not bugs—they are irreducible ambiguity that the labeling system must measure, manage, and mitigate. The infrastructure supporting labeling operations must therefore handle not just throughput (millions of examples) but also quality control (inter-annotator agreement), cost management (the 1,000$\times$ Rule from our data engineering constants), and governance (privacy, consent, and bias monitoring). +Unlike automated transformations that can be parallelized across machines, labeling introduces human judgment into the pipeline, creating unique engineering challenges. A crowdsourced annotator might mislabel a whispered "Alexa" as background noise. An expert radiologist might disagree with a colleague about a borderline diagnosis. These disagreements are not bugs—they are irreducible ambiguity that the labeling system must measure, manage, and mitigate. The infrastructure supporting labeling operations must therefore handle not just throughput (millions of examples) but also quality control (inter-annotator agreement), cost management (the 1,000 $\times$ Rule from our data engineering constants), and governance (privacy, consent, and bias monitoring). ### Label Types and System Requirements {#sec-data-engineering-label-types-system-requirements-4c33} @@ -2570,9 +2570,9 @@ Building effective labeling systems requires understanding how different label t Classification labels\index{Classification Labels!storage requirements} represent the simplest form, categorizing images with a specific tag or (in multi-label classification) tags such as labeling an image as "car" or "pedestrian." While conceptually straightforward, a production system processing millions of video frames must efficiently store and retrieve these labels. Storage requirements are modest (a single integer or string per image), but retrieval patterns matter: training often samples random subsets while validation requires sequential access to all labels, driving different indexing strategies. -Bounding boxes\index{Bounding Box Annotation!localization}\index{Object Detection!bounding boxes} extend beyond simple classification by identifying object locations, drawing a box around each object of interest. Our system now needs to track not just what objects exist, but where they are in each frame. This spatial information introduces new storage and processing challenges, especially when tracking moving objects across video frames. Each bounding box requires storing four coordinates (x, y, width, height) plus the object class, multiplying storage by 5$\times$ compared to classification. More importantly, bounding box annotation requires pixel-precise positioning that takes 10–20$\times$ longer than classification, dramatically affecting labeling throughput and cost. +Bounding boxes\index{Bounding Box Annotation!localization}\index{Object Detection!bounding boxes} extend beyond simple classification by identifying object locations, drawing a box around each object of interest. Our system now needs to track not just what objects exist, but where they are in each frame. This spatial information introduces new storage and processing challenges, especially when tracking moving objects across video frames. Each bounding box requires storing four coordinates (x, y, width, height) plus the object class, multiplying storage by 5 $\times$ compared to classification. More importantly, bounding box annotation requires pixel-precise positioning that takes 10–20 $\times$ longer than classification, dramatically affecting labeling throughput and cost. -Segmentation maps\index{Semantic Segmentation!pixel-level labels} provide the most comprehensive information by classifying objects at the pixel level, highlighting each object in a distinct color. For our traffic monitoring system, this might mean precisely outlining each vehicle, pedestrian, and road sign. These detailed annotations significantly increase our storage and processing requirements. A segmentation mask for a 1920 $\times$ 1080 image requires 2 million labels (one per pixel), compared to perhaps 10 bounding boxes or a single classification label. This ~200,000$\times$ storage increase relative to bounding boxes—and the hours required per image for manual segmentation—make this approach suitable only when pixel-level precision is essential. +Segmentation maps\index{Semantic Segmentation!pixel-level labels} provide the most comprehensive information by classifying objects at the pixel level, highlighting each object in a distinct color. For our traffic monitoring system, this might mean precisely outlining each vehicle, pedestrian, and road sign. These detailed annotations significantly increase our storage and processing requirements. A segmentation mask for a 1920 $\times$ 1080 image requires 2 million labels (one per pixel), compared to perhaps 10 bounding boxes or a single classification label. This ~200,000 $\times$ storage increase relative to bounding boxes—and the hours required per image for manual segmentation—make this approach suitable only when pixel-level precision is essential. ![**Data Annotation Granularity**: Three versions of the same street scene show increasing annotation detail: a simple classification label, bounding boxes around vehicles and pedestrians, and pixel-level semantic segmentation with distinct colors. Each level increases labeling cost and storage requirements while providing richer training signal.](images/png/cs249r_labels_new.png){#fig-labels width=90% fig-alt="Three versions of same street scene showing increasing annotation detail: simple classification label, bounding boxes around vehicles and pedestrians, and pixel-level semantic segmentation with distinct colors."} @@ -2595,7 +2595,7 @@ Beyond random sampling approaches, collecting multiple labels per data point, of [^fn-fleiss-kappa]: **Fleiss' kappa**: Named after biostatistician Joseph L. Fleiss who generalized Cohen's kappa in 1971 to handle multiple raters simultaneously. While Cohen's kappa works only for two raters, Fleiss extended the mathematics to any number of annotators, making it essential for crowdsourced labeling where different subsets of workers label each example. Values range from -1 to 1: above 0.8 indicates strong agreement, 0.6-0.8 moderate, and below 0.4 suggests labeling guidelines need clarification. -The consensus approach reflects an economic trade-off essential for scalable systems. Expert review costs 10–50$\times$ more per example than crowdsourced labeling, but forcing agreement on ambiguous examples through majority voting of non-experts produces systematically biased labels. By routing only genuinely ambiguous cases to experts—often 5-15% of examples identified through low inter-annotator agreement—systems balance cost against quality. This tiered approach enables processing millions of examples economically while maintaining quality standards through targeted expert intervention. +The consensus approach reflects an economic trade-off essential for scalable systems. Expert review costs 10–50 $\times$ more per example than crowdsourced labeling, but forcing agreement on ambiguous examples through majority voting of non-experts produces systematically biased labels. By routing only genuinely ambiguous cases to experts—often 5-15% of examples identified through low inter-annotator agreement—systems balance cost against quality. This tiered approach enables processing millions of examples economically while maintaining quality standards through targeted expert intervention. While technical infrastructure provides the foundation for quality control, successful labeling systems must also consider human factors. When working with annotators, organizations need reliable systems for training and guidance. This includes good documentation with clear examples of correct labeling, visual demonstrations of edge cases and how to handle them, regular feedback mechanisms showing annotators their accuracy on gold standard examples, and calibration sessions where annotators discuss ambiguous cases to develop shared understanding. For complex or domain-specific tasks, the system might implement tiered access levels, routing challenging cases to annotators with appropriate expertise based on their demonstrated accuracy on similar examples. @@ -2713,13 +2713,13 @@ Methods such as active learning\index{Active Learning!label prioritization}[^fn- **The Physics**: -1. **Sample Efficiency**: Active learning typically achieves target accuracy with **5–10$\times$ fewer samples** than random selection. +1. **Sample Efficiency**: Active learning typically achieves target accuracy with **5–10 $\times$ fewer samples** than random selection. 2. **Cost per Point**: Random sampling = $0.50/label. Active learning adds compute cost (~$0.01/image for inference) to find hard examples. 3. **The Multiplier**: * **Random**: To reach 95%, you might need 1M labels ($500K). **Budget Exceeded.** * **Active**: You need ~100K–200K *hard* examples ($50K–$100K). **Feasible.** -**The Engineering Conclusion**: Algorithm choice is a 10$\times$ lever on data budget. Spending 10% of your budget on compute to select data saves 90% of your budget on human labeling. +**The Engineering Conclusion**: Algorithm choice is a 10 $\times$ lever on data budget. Spending 10% of your budget on compute to select data saves 90% of your budget on human labeling. ::: Quality control becomes increasingly important as these AI components interact. The system must monitor both AI and human performance through systematic metrics. Model confidence calibration matters: if the AI reports 95% confidence but achieves only 75% accuracy at that confidence level, pre-annotations mislead human reviewers. Human-AI agreement rates reveal whether AI assistance helps or hinders: when humans frequently override AI suggestions, the pre-annotations may be introducing bias rather than accelerating work. These metrics require careful instrumentation throughout the labeling pipeline, tracking not just final labels but the interaction between human and AI at each stage. @@ -2813,7 +2813,7 @@ Beyond the functional differences between storage systems, cost and performance : **Storage Cost-Performance Trade-offs**: Different storage tiers provide distinct cost-performance characteristics that determine their suitability for specific ML workloads. Training data loading requires high-throughput sequential access—an I/O pattern that must align with the **Accelerator Memory Hierarchy** (@sec-hardware-acceleration)—while online serving needs low-latency random reads, while archival storage prioritizes cost over access speed for compliance and historical data. {#tbl-storage-performance} -@tbl-storage-performance reveals why ML systems employ tiered storage architectures. Consider the economics of storing our KWS training dataset (`{python} kws_dataset_size_gb_str` GB): object storage costs \$`{python} kws_storage_s3_cost_str`/month, enabling affordable long-term retention of raw audio, while maintaining working datasets on NVMe[^fn-nvme] for active training costs \$`{python} kws_storage_nvme_low_cost_str`–`{python} kws_storage_nvme_high_cost_str`/month but provides 50$\times$ faster data loading. +@tbl-storage-performance reveals why ML systems employ tiered storage architectures. Consider the economics of storing our KWS training dataset (`{python} kws_dataset_size_gb_str` GB): object storage costs \$`{python} kws_storage_s3_cost_str`/month, enabling affordable long-term retention of raw audio, while maintaining working datasets on NVMe[^fn-nvme] for active training costs \$`{python} kws_storage_nvme_low_cost_str`–`{python} kws_storage_nvme_high_cost_str`/month but provides 50 $\times$ faster data loading. [^fn-nvme]: **Non-Volatile Memory Express (NVMe)**: A storage protocol designed specifically for flash memory, bypassing the legacy AHCI interface that SATA SSDs use. NVMe connects directly to the PCIe bus, enabling 64K command queues versus SATA's single queue, reducing latency from milliseconds to microseconds. For ML training workloads, NVMe's 5-7 GB/s sequential throughput prevents storage from bottlenecking GPU utilization, while SATA SSD's 500 MB/s limit would leave expensive accelerators idle waiting for data. @@ -2850,7 +2850,7 @@ obj_load_str = fmt(obj_load_s_value, precision=0, commas=True) load_speedup_str = fmt(load_speedup_value, precision=0, commas=False) ``` -The performance difference directly impacts iteration velocity. Training that loads data at `{python} nvme_bw_gbs_value` GB/s completes dataset loading in `{python} nvme_load_str` seconds, compared to `{python} obj_load_str` seconds at typical object storage speeds. This `{python} load_speedup_str`$\times$ difference determines whether teams can iterate multiple times daily or must wait hours between experiments. +The performance difference directly impacts iteration velocity. Training that loads data at `{python} nvme_bw_gbs_value` GB/s completes dataset loading in `{python} nvme_load_str` seconds, compared to `{python} obj_load_str` seconds at typical object storage speeds. This `{python} load_speedup_str` $\times$ difference determines whether teams can iterate multiple times daily or must wait hours between experiments. To build engineering judgment, practitioners must internalize the orders of magnitude separating these tiers. @tbl-ml-latencies translates these disparities into human-scale analogies that build intuition for system design: if a CPU cycle were one second, fetching from local SSD would take two days, while a cross-country network request would span six years. Internalizing these ratios—three orders of magnitude between L1 cache and DRAM, another three between DRAM and SSD—explains why seemingly small architectural choices cascade into large performance differences. @@ -2865,7 +2865,7 @@ To build engineering judgment, practitioners must internalize the orders of magn | **Object Store (S3)** | 20,000,000 | 1 year | Archival access | | **Internet (CA to VA)** | 100,000,000 | 6 years | Global user experience | -: **Latency Numbers Every ML Systems Engineer Should Know**: Understanding the quantitative disparities in the storage hierarchy is essential for diagnosing bottlenecks. If a CPU cycle were 1 second, fetching data from local SSD would be like waiting 2 days, while a cross-country network request would take 6 years. Source: Adapted from Jeff Dean's "Numbers Every Programmer Should Know". {#tbl-ml-latencies} +: **Latency Numbers Every ML Systems Engineer Should Know**: Understanding the quantitative disparities in the storage hierarchy is essential for diagnosing bottlenecks. If a CPU cycle were 1 second, fetching data from local SSD would be like waiting 2 days, while a cross-country network request would take 6 years. Source: Adapted from Jeff Dean's "Numbers Every Programmer Should Know". For the full hardware reference with bandwidth and energy ratios, see @sec-machine-foundations-numbers-know-b531. {#tbl-ml-latencies} These latency numbers originated from Jeff Dean's influential 2009 talk at Stanford,[^fn-jeff-dean] which established the quantitative culture that distinguishes systems engineering from programming. Understanding them is foundational to ML systems engineering: they explain why a poorly designed storage architecture can idle a $15,000 GPU, and why distributed training requires careful attention to data locality. @@ -2959,6 +2959,7 @@ Designing for high-throughput training requires calculating a *storage bandwidth **The Systems Conclusion:** To saturate a *single* A100, your storage must deliver **`{python} required_bw_gbs_str` GB/s**. + * **S3 Standard**: ~`{python} s3_bw_mbs_str` MB/s per thread. You need **`{python} s3_workers_str` concurrent worker threads**. * **SATA SSD**: ~`{python} sata_bw_mbs_str` MB/s sequential. Totally insufficient (bottleneck). * **NVMe SSD**: ~3-7 GB/s. **Required.** @@ -3029,9 +3030,9 @@ $$ \text{Effective Bandwidth} = \text{Physical Bandwidth} \times \eta_{format} $ - **Result**: You waste `{python} waste_pct_str`% of your disk bandwidth. - **Column-Oriented (Parquet)**: Reads only the `{python} needed_cols_str` columns needed. - eta_format ≈ `{python} eta_parquet_str` (ignoring metadata overhead). - - **Result**: You get **`{python} throughput_ratio_str`$\times$ higher effective throughput**. + - **Result**: You get **`{python} throughput_ratio_str` $\times$ higher effective throughput**. -**The Systems Conclusion**: Switching from CSV to Parquet is not just a file change; it is mathematically equivalent to buying a **`{python} throughput_ratio_str`$\times$ faster hard drive**. The serialization overhead of different formats is quantified in @tbl-serialization-cost in @sec-data-foundations. For a deeper treatment of row vs. columnar storage layouts and the algebra of data operations (selection, projection, join), see @sec-data-foundations-row-vs-columnar-formats-bca2. +**The Systems Conclusion**: Switching from CSV to Parquet is not just a file change; it is mathematically equivalent to buying a **`{python} throughput_ratio_str` $\times$ faster hard drive**. The serialization overhead of different formats is quantified in @tbl-serialization-cost in @sec-data-foundations. For a deeper treatment of row vs. columnar storage layouts and the algebra of data operations (selection, projection, join), see @sec-data-foundations-row-vs-columnar-formats-bca2. ::: Columnar storage formats\index{Parquet!columnar format}\index{ORC!columnar format} like Parquet or ORC deliver this five to 10 times I/O reduction compared to row-based formats like CSV or JSON for typical ML workloads. The reduction comes from two mechanisms: reading only required columns rather than entire records, and column-level compression exploiting value patterns within columns. Consider a fraud detection dataset with 100 columns where models typically use 20 features—columnar formats read only needed columns, achieving 80% I/O reduction before compression. Column compression proves particularly effective for categorical features with limited cardinality: a country code column with 200 unique values in 100 million records compresses 20 to 50 times through dictionary encoding, while run-length encoding compresses sorted columns by storing only value changes. The combination can achieve total I/O reduction of 20 to 100 times compared to uncompressed row formats, directly translating to faster training iterations and reduced infrastructure costs. @@ -3324,7 +3325,7 @@ The patterns in this section—data debt accumulation, diagnostic debugging, and **Fallacy:** *More data always improves model performance.* -Beyond a threshold, additional data yields diminishing returns while costs scale linearly. A dataset of 10 million examples may provide only marginal accuracy gains over 1 million examples, yet incur 10$\times$ the storage, labeling, and processing costs. The Information Entropy concept from @sec-data-engineering-physics-data-cdcb explains why: if new examples are redundant (low entropy), they add mass without information. Smart data selection—active learning, deduplication, curriculum design—often outperforms naive data accumulation. +Beyond a threshold, additional data yields diminishing returns while costs scale linearly. A dataset of 10 million examples may provide only marginal accuracy gains over 1 million examples, yet incur 10 $\times$ the storage, labeling, and processing costs. The Information Entropy concept from @sec-data-engineering-physics-data-cdcb explains why: if new examples are redundant (low entropy), they add mass without information. Smart data selection—active learning, deduplication, curriculum design—often outperforms naive data accumulation. **Pitfall:** *Treating data preprocessing as a one-time task.* @@ -3358,9 +3359,9 @@ The technical architecture of data systems demonstrates how engineering decision * **Pipeline architecture choices have large cost implications.** Real-time streaming increases operational cost and complexity relative to batch, while ETL reduces storage footprint at the expense of higher engineering overhead during schema changes. Select ingestion patterns based on the value of latency, not the appeal of real-time. -* **Labeling costs dominate and require substantial resource allocation.** Labeling typically costs 1,000–3,000$\times$ more than model training compute. Labeling is the serial bottleneck that parallelization cannot solve. +* **Labeling costs dominate and require substantial resource allocation.** Labeling typically costs 1,000–3,000 $\times$ more than model training compute. Labeling is the serial bottleneck that parallelization cannot solve. -* **Storage hierarchy determines iteration speed.** The 70$\times$ throughput gap between local NVMe (7 GB/s) and cloud object storage (100 MB/s) determines whether iterations occur daily or weekly. +* **Storage hierarchy determines iteration speed.** The 70 $\times$ throughput gap between local NVMe (7 GB/s) and cloud object storage (100 MB/s) determines whether iterations occur daily or weekly. * **Data debt compounds and requires continuous remediation.** Documentation, schema, quality, and freshness debt accumulate with compound interest. Allocate sustained engineering capacity to prevent remediation from overwhelming new feature work. diff --git a/book/quarto/contents/vol1/data_selection/data_selection.qmd b/book/quarto/contents/vol1/data_selection/data_selection.qmd index 2ec02aad5e..da05e92b06 100644 --- a/book/quarto/contents/vol1/data_selection/data_selection.qmd +++ b/book/quarto/contents/vol1/data_selection/data_selection.qmd @@ -61,7 +61,7 @@ The practical consequences are enormous: compressing models and accelerating har Data selection asks a deceptively simple question with profound engineering consequences: given a clean, well-engineered dataset, which examples contribute the most learning per unit of compute cost? The preceding chapter on data engineering (@sec-data-engineering) established the infrastructure for collecting, cleaning, and preparing data, producing pipelines that ingest raw signals and yield well-governed, versioned datasets ready for training. That chapter ensured data *quality* through correct labels, consistent schemas, and clean records. Data selection optimizes data *value* by extracting maximum learning from minimum samples, directly shrinking the **Total Operations ($O$)** term in the **Iron Law** (@sec-introduction-iron-law-ml-systems-c32a). The distinction matters: quality asks *whether* data is correct, while value asks *whether* correct data is worth the compute spent processing it. \index{Scaling Laws!data-compute asymmetry} -For decades, the dominant strategy was straightforward: more data, better models. Scaling laws [@kaplan2020scaling; @hoffmann2022training] confirmed that model performance improves predictably with dataset size, and teams responded rationally by scraping more web pages, labeling more images, and generating more synthetic examples. A critical asymmetry has since emerged. Hardware acceleration (@sec-hardware-acceleration) has outpaced the growth of high-quality data. GPU compute capacity has increased faster than traditional Moore's Law projections (architectural innovations like Tensor Cores and reduced-precision arithmetic stack on top of process-node gains, yielding effective throughput improvements faster than every two years) while the supply of novel, high-quality human-generated text and images grows at roughly 2$\times$ every five years (@tbl-scaling-asymmetry). The internet has already been scraped. Domain experts cannot label faster. This asymmetry, which researchers call the **Data Wall**\index{Data Wall!definition}[^fn-data-wall] [@villalobos2022will], has inverted the optimization priority from "get more data" to "get more from existing data." +For decades, the dominant strategy was straightforward: more data, better models. Scaling laws [@kaplan2020scaling; @hoffmann2022training] confirmed that model performance improves predictably with dataset size, and teams responded rationally by scraping more web pages, labeling more images, and generating more synthetic examples. A critical asymmetry has since emerged. Hardware acceleration (@sec-hardware-acceleration) has outpaced the growth of high-quality data. GPU compute capacity has increased faster than traditional Moore's Law projections (architectural innovations like Tensor Cores and reduced-precision arithmetic stack on top of process-node gains, yielding effective throughput improvements faster than every two years) while the supply of novel, high-quality human-generated text and images grows at roughly 2 $\times$ every five years (@tbl-scaling-asymmetry). The internet has already been scraped. Domain experts cannot label faster. This asymmetry, which researchers call the **Data Wall**\index{Data Wall!definition}[^fn-data-wall] [@villalobos2022will], has inverted the optimization priority from "get more data" to "get more from existing data." ```{python} #| label: scaling-asymmetry-calc @@ -251,7 +251,7 @@ compute_gap_str = ComputeDataGap.compute_gap_str training_months_value = ComputeDataGap.training_months_value ``` -The compute budget (`{python} h100_count_str` H100 GPUs for `{python} training_months_value` months) represents tens of millions of dollars and can process over `{python} tokens_capacity_str` tokens. Yet only ~`{python} tokens_available_str` tokens of deduplicated, filtered web text exist, leaving a `{python} compute_gap_str`$\times$ gap between what compute can process and what quality data can fill. The team faces three options: train on the same data for multiple epochs (diminishing returns after epochs 2--3), lower quality thresholds to include more data (degrades model quality), or invest in data selection through better filtering, curriculum design, and synthetic augmentation to extract more learning from each token. The third option is increasingly the dominant approach. +The compute budget (`{python} h100_count_str` H100 GPUs for `{python} training_months_value` months) represents tens of millions of dollars and can process over `{python} tokens_capacity_str` tokens. Yet only ~`{python} tokens_available_str` tokens of deduplicated, filtered web text exist, leaving a `{python} compute_gap_str` $\times$ gap between what compute can process and what quality data can fill. The team faces three options: train on the same data for multiple epochs (diminishing returns after epochs 2--3), lower quality thresholds to include more data (degrades model quality), or invest in data selection through better filtering, curriculum design, and synthetic augmentation to extract more learning from each token. The third option is increasingly the dominant approach. This data selection imperative applies across model architectures, though the bottlenecks differ. Unlike our compute-bound ResNet-50 Lighthouse, GPT-2/Llama models are **memory bandwidth-bound** during inference (though often compute-bound during training as well) and still benefit enormously from data selection during training. Each token processed requires the same forward/backward pass cost regardless of model bottleneck, so fewer tokens means fewer FLOPs. This universality (data selection benefits every architecture, regardless of its dominant bottleneck) motivates a broader framing: data selection as a *systems* problem rather than a purely statistical one. @@ -341,7 +341,7 @@ In the **Iron Law of ML Systems** ($T = \frac{D_{vol}}{BW} + \frac{O}{R_{peak} \ - **Data selection**: Reduces the number of passes through the entire equation \index{Iron Law!multiplicative savings from data selection} -This makes data selection multiplicatively valuable: when all three optimizations act on the same bottleneck, a 2$\times$ reduction in dataset size with 2$\times$ model compression and 2$\times$ hardware acceleration yields `{python} combined_factor_str`$\times$ total cost reduction, not 6$\times$. +This makes data selection multiplicatively valuable: when all three optimizations act on the same bottleneck, a 2 $\times$ reduction in dataset size with 2 $\times$ model compression and 2 $\times$ hardware acceleration yields `{python} combined_factor_str` $\times$ total cost reduction, not 6 $\times$. ::: Consider training cost reduction: a `{python} dataset_reduction_pct_str`% reduction in dataset size does not merely improve sample efficiency; it directly halves the number of forward passes, backward passes, and gradient updates. For a USD `{python} training_cost_m_str` M training run, this translates to USD `{python} compute_savings_m_str` M in compute savings. The relationship is linear and immediate. @@ -349,7 +349,7 @@ Consider training cost reduction: a `{python} dataset_reduction_pct_str`% reduct \index{Deduplication!storage and I/O cost reduction} \index{Active Learning!labeling cost reduction} \index{Green AI!data selection as energy reduction} -These compute savings cascade through the entire infrastructure stack. Large datasets consume petabytes of storage and saturate network bandwidth during distributed training; deduplication and coreset selection reduce storage costs while eliminating I/O bottlenecks that can idle expensive GPU clusters. The savings extend to labeling economics: expert labeling costs (\$5--100+ per sample in domains like medical imaging) often exceed compute costs, and active learning and semi-supervised methods reduce labeling budgets by 10–100$\times$. The environmental implications compound further: training a large language model can emit hundreds of tons of CO₂, making data selection the most direct lever for Green AI, since halving the dataset halves training energy with no accuracy trade-off if done correctly. Smaller curated datasets also enable faster iteration velocity. A team that can iterate in hours rather than days has a compounding advantage in model development. +These compute savings cascade through the entire infrastructure stack. Large datasets consume petabytes of storage and saturate network bandwidth during distributed training; deduplication and coreset selection reduce storage costs while eliminating I/O bottlenecks that can idle expensive GPU clusters. The savings extend to labeling economics: expert labeling costs (\$5--100+ per sample in domains like medical imaging) often exceed compute costs, and active learning and semi-supervised methods reduce labeling budgets by 10–100 $\times$. The environmental implications compound further: training a large language model can emit hundreds of tons of CO₂, making data selection the most direct lever for Green AI, since halving the dataset halves training energy with no accuracy trade-off if done correctly. Smaller curated datasets also enable faster iteration velocity. A team that can iterate in hours rather than days has a compounding advantage in model development. These cascading benefits illustrate a broader point: where the ML researcher asks "what is the sample complexity of this learning problem?", the systems engineer asks "what is the cost-per-accuracy-point across the entire pipeline, from data acquisition through deployment?" This chapter equips you with the systems engineer's toolkit for that question: techniques to minimize total cost, metrics to quantify efficiency gains, and architectural patterns to implement data selection at scale. @@ -365,6 +365,7 @@ In the optimization triad (@fig-optimization-triad), data selection plays the ro ```{python} #| label: fig-optimization-triad #| echo: false +#| out-width: "70%" #| fig-cap: "**The Optimization Triad**: Machine learning performance relies on three pillars: Algorithms (models), Machine (hardware/software), and Data Selection. While algorithms and machines have traditionally received the most attention, optimizing data selection (Input Optimization) offers a third, powerful lever for scaling performance." #| fig-alt: "A triangular diagram with three nodes: Algorithms (Model), Machine (Hardware), and Data Selection. Bidirectional arrows connect all three with edge labels: Compute Bound between Algorithms and Machine, I/O Bound between Machine and Data Selection, and Sample Efficiency between Data Selection and Algorithms. Data Selection is highlighted with a bold border. ML Performance appears at the center." @@ -373,8 +374,8 @@ import matplotlib.patches as mpatches from mlsys import viz fig, ax, COLORS, plt = viz.setup_plot(figsize=(5, 4.5)) -ax.set_xlim(-3, 3) -ax.set_ylim(-2.5, 3) +ax.set_xlim(-2.75, 2.75) +ax.set_ylim(-2.45, 2.8) ax.set_aspect('equal') ax.axis('off') ax.grid(False) @@ -413,6 +414,7 @@ draw_arrow(br, top, 'Sample\nEfficiency', 'right', offset=(0.45, 0.05)) # Center label ax.text(0, -0.15, 'ML\nPerformance', ha='center', va='center', fontsize=10, fontweight='bold', color=COLORS['primary']) +fig.tight_layout(pad=0.1) plt.show() ``` @@ -423,16 +425,12 @@ $$ $$ \index{Information-Compute Ratio!equivalence to hardware speedup} -As detailed in the "Data Selection and the Iron Law" callout above, data selection turns the Total Operations ($O$) term from a fixed constant into a variable. By maximizing ICR, we reduce the total FLOPs required to reach a target performance level. A 2$\times$ improvement in ICR is mathematically equivalent to a 2$\times$ improvement in hardware Peak Throughput ($R_{peak}$), but often much cheaper to achieve. Note that ICR focuses specifically on the compute component of the broader Selection Efficiency metric defined earlier, which also accounts for acquisition, labeling, and storage costs. +As detailed in the "Data Selection and the Iron Law" callout above, data selection turns the Total Operations ($O$) term from a fixed constant into a variable. By maximizing ICR, we reduce the total FLOPs required to reach a target performance level. A 2 $\times$ improvement in ICR is mathematically equivalent to a 2 $\times$ improvement in hardware Peak Throughput ($R_{peak}$), but often much cheaper to achieve. Note that ICR focuses specifically on the compute component of the broader Selection Efficiency metric defined earlier, which also accounts for acquisition, labeling, and storage costs. A random batch of raw data often has low ICR: it contains redundant examples, noisy samples, or "easy" examples the model has already mastered, wasting GPU cycles on zero-information updates. High-efficiency data pipelines (@fig-data-selection-pipeline) filter, order, and synthesize data to maximize ICR, ensuring that every FLOP contributes to learning. To illustrate, consider *computing ICR* on a concrete coreset selection task. Later in this chapter, @sec-data-selection-measurement-framework-733b provides the complete measurement framework for evaluating these efficiency gains, including the compute-optimal frontier diagnostic that determines whether training is data-starved or compute-starved. -```{tikz} -#| label: fig-data-selection-pipeline -#| echo: false -#| fig-cap: "**The Data Selection Pipeline**: A structured approach to increasing data value. Raw data is first pruned to remove redundancy (Static Pruning), then dynamically selected during training (Active Learning), and finally augmented to increase diversity (Synthesis). Each stage increases the Information-Compute Ratio (ICR)." -#| fig-alt: "A flow diagram showing the progression of data: Raw Data -> Static Pruning -> Dynamic Selection -> Synthetic Generation -> High Value Model. Arrows indicate the flow." - +::: {#fig-data-selection-pipeline fig-cap="**The Data Selection Pipeline**: A structured approach to increasing data value. Raw data is first pruned to remove redundancy (Static Pruning), then dynamically selected during training (Active Learning), and finally augmented to increase diversity (Synthesis). Each stage increases the Information-Compute Ratio (ICR)." fig-alt="A flow diagram showing the progression of data: Raw Data -> Static Pruning -> Dynamic Selection -> Synthetic Generation -> High Value Model. Arrows indicate the flow."} +```{=latex} \begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}, >=stealth] \definecolor{GreenLine}{HTML}{008F45} \definecolor{GreenL}{HTML}{D4EFDF} @@ -479,6 +477,7 @@ A random batch of raw data often has low ICR: it contains redundant examples, no \end{tikzpicture} ``` +::: The following checkpoint verifies understanding of the core ICR concept before the calculation examples. @@ -488,7 +487,7 @@ The goal of data selection is to maximize the ICR. **Metrics** - [ ] **ICR Application**: Given two training runs with identical accuracy gains but different compute budgets, can you determine which had higher ICR? -- [ ] **Data Efficiency**: Do you understand why a 50% smaller dataset with 2$\times$ higher ICR yields the same model for half the training cost? +- [ ] **Data Efficiency**: Do you understand why a 50% smaller dataset with 2 $\times$ higher ICR yields the same model for half the training cost? **The Pipeline** @@ -580,7 +579,7 @@ coreset_pct_str = fmt(coreset_fraction_value * 100, precision=0, commas=False) - Compute: `{python} coreset_size_str` $\times$ `{python} resnet50_fwdbwd_gflops_str` GFLOPs = **`{python} coreset_flops_str` FLOPs** - ICR_coreset = `{python} acc_gain_coreset_str` / (`{python} coreset_flops_str`) = **`{python} icr_coreset_str` per FLOP** -**Result**: The coreset achieves **`{python} icr_ratio_str`$\times$ higher ICR**, nearly twice the learning per FLOP, by eliminating low-information "easy" samples that contribute little to the decision boundary. The `{python} acc_diff_str` percentage point accuracy difference is often acceptable given the `{python} coreset_pct_str`% compute savings. +**Result**: The coreset achieves **`{python} icr_ratio_str` $\times$ higher ICR**, nearly twice the learning per FLOP, by eliminating low-information "easy" samples that contribute little to the decision boundary. The `{python} acc_diff_str` percentage point accuracy difference is often acceptable given the `{python} coreset_pct_str`% compute savings. ::: The remainder of this chapter explores each stage of the three-stage optimization pipeline introduced above (static pruning, dynamic selection, and synthetic generation) in depth. We begin with static pruning, the techniques that can reduce a dataset by 30 to 50 percent before training even begins. @@ -595,7 +594,7 @@ Before a single gradient is computed, significant efficiency gains are available \index{Data Redundancy!empirical evidence} The most counterintuitive finding in data selection is that training on *less* data often produces models just as accurate as training on the full dataset. Practitioners have long assumed that more data yields better performance, and while this holds in many scenarios, it obscures a critical reality: typical large-scale datasets contain massive redundancy. Empirical studies on coreset selection and data pruning have consistently demonstrated this redundancy across standard benchmarks. -On CIFAR-10, gradient-based selection methods (EL2N, GraNd) [@paul2021deep] have shown that training on 50% of carefully selected samples matches the accuracy of the full dataset, with aggressive pruning reaching 10--30% of samples while retaining 90%+ of original performance. ImageNet-1K presents a harder challenge because it is less redundant, yet researchers have demonstrated that 20--30% of ImageNet can be pruned with negligible loss, and up to 50% reduction is possible with a small accuracy trade-off (~1 percentage point), yielding 2$\times$ fewer training FLOPs [@paul2021deep; @sorscher2022beyond]. The pattern extends to language modeling: web-scraped corpora like The Pile[^fn-the-pile] and C4[^fn-c4] contain substantial exact and near-duplicate content, and deduplication studies [@lee2022deduplicating] report 10--30% redundancy ratios, with deduplicated training yielding *better* downstream performance through less memorization and more generalization. +On CIFAR-10, gradient-based selection methods (EL2N, GraNd) [@paul2021deep] have shown that training on 50% of carefully selected samples matches the accuracy of the full dataset, with aggressive pruning reaching 10--30% of samples while retaining 90%+ of original performance. ImageNet-1K presents a harder challenge because it is less redundant, yet researchers have demonstrated that 20--30% of ImageNet can be pruned with negligible loss, and up to 50% reduction is possible with a small accuracy trade-off (~1 percentage point), yielding 2 $\times$ fewer training FLOPs [@paul2021deep; @sorscher2022beyond]. The pattern extends to language modeling: web-scraped corpora like The Pile[^fn-the-pile] and C4[^fn-c4] contain substantial exact and near-duplicate content, and deduplication studies [@lee2022deduplicating] report 10--30% redundancy ratios, with deduplicated training yielding *better* downstream performance through less memorization and more generalization. [^fn-the-pile]: **The Pile**: A 825 GB English text dataset created by EleutherAI (an open-source AI research lab) and released in December 2020. The name evokes a deliberately enormous "pile" of diverse text. The Pile aggregates 22 sub-datasets spanning academic papers (PubMed, ArXiv), books (Project Gutenberg, Books3), code (GitHub), web text (CommonCrawl), and specialized sources (Stack Exchange, Wikipedia, USPTO patents). Its design philosophy prioritizes *diversity* over scale: rather than simply crawling more web pages, The Pile combines curated sources covering different domains, writing styles, and knowledge areas. The dataset became widely used for training open-source language models (GPT-Neo, GPT-J, Pythia) and established that data quality and diversity matter as much as raw volume. @@ -666,8 +665,8 @@ ratio_str = QualityMultiplier.ratio_str **The Physics of Noise**: Why is one clean sample worth 100 noisy ones? **The Math**: Classical learning theory (for convex optimization with SGD) tells us that convergence rates depend on label noise. While deep learning operates in a non-convex regime, the qualitative relationship holds broadly. -1. **Clean Data**: Convergence rate is typically $O(1/N)$. To halve the error, you need **2$\times$** data. -2. **Noisy Data**: Convergence rate drops to $O(1/\sqrt{N})$. To halve the error, you need **4$\times$** data. +1. **Clean Data**: Convergence rate is typically $O(1/N)$. To halve the error, you need **2 $\times$** data. +2. **Noisy Data**: Convergence rate drops to $O(1/\sqrt{N})$. To halve the error, you need **4 $\times$** data. **The Multiplier**: To reach a target error $\epsilon$: @@ -679,9 +678,9 @@ To reach a target error $\epsilon$: * $N_{clean}$ ≈ `{python} n_clean_str` * $N_{noisy}$ ≈ `{python} n_noisy_str` -* **Ratio**: `{python} ratio_str`$\times$ more data required if noisy. +* **Ratio**: `{python} ratio_str` $\times$ more data required if noisy. -**The Systems Conclusion**: Cleaning your data (removing label noise) is a **`{python} ratio_str`$\times$ compute accelerator**. +**The Systems Conclusion**: Cleaning your data (removing label noise) is a **`{python} ratio_str` $\times$ compute accelerator**. ::: The practical question then becomes: *how* do we identify which samples to keep? @@ -859,7 +858,7 @@ n_epochs_proxy_str = fmt(n_epochs_proxy_value, precision=0, commas=False) # e.g @lst-el2n-coreset demonstrates how to compute EL2N scores and select a coreset using a lightweight proxy model. The code shows two functions: `compute_el2n_scores` trains a proxy model briefly and measures prediction confidence via L2 distance from one-hot labels, while `select_coreset` retains only the highest-uncertainty samples. -::: {#lst-el2n-coreset lst-cap="**EL2N-Based Coreset Selection**: Computing uncertainty scores with a proxy model enables 10$\times$ data reduction while preserving accuracy. The `compute_el2n_scores` function trains a small model for a few epochs, then measures prediction confidence via L2 distance from one-hot labels. High scores indicate uncertain samples near decision boundaries. The `select_coreset` function retains only these informative samples, discarding redundant easy examples."} +::: {#lst-el2n-coreset lst-cap="**EL2N-Based Coreset Selection**: Computing uncertainty scores with a proxy model enables 10 $\times$ data reduction while preserving accuracy. The `compute_el2n_scores` function trains a small model for a few epochs, then measures prediction confidence via L2 distance from one-hot labels. High scores indicate uncertain samples near decision boundaries. The `select_coreset` function retains only these informative samples, discarding redundant easy examples."} ```{.python} def compute_el2n_scores(model, dataloader, num_epochs=5): """Compute EL2N scores. @@ -1051,12 +1050,8 @@ Curriculum learning optimizes the order in which samples are presented but assum Unlike static pruning, which discards samples permanently, active learning maintains an unlabeled pool and queries it strategically over time. Follow the cycle in @fig-active-learning-loop: the model's current uncertainty determines what gets labeled next, creating a feedback loop where each labeling round improves the model's ability to identify what it still needs to learn. -```{tikz} -#| label: fig-active-learning-loop -#| echo: false -#| fig-cap: "**Active Learning Loop**: Instead of labeling all data, the model selects the most 'confusing' or informative samples from an unlabeled pool. These samples are sent to an Oracle (human annotator) and added to the training set. The model is retrained, and the cycle repeats, creating a feedback loop that maximizes information gain per label." -#| fig-alt: "A cycle diagram: Unlabeled Pool -> Selection Strategy -> Oracle -> Labeled Set -> Model Training -> back to Selection Strategy." - +::: {#fig-active-learning-loop fig-cap="**Active Learning Loop**: Instead of labeling all data, the model selects the most 'confusing' or informative samples from an unlabeled pool. These samples are sent to an Oracle (human annotator) and added to the training set. The model is retrained, and the cycle repeats, creating a feedback loop that maximizes information gain per label." fig-alt="A cycle diagram: Unlabeled Pool -> Selection Strategy -> Oracle -> Labeled Set -> Model Training -> back to Selection Strategy."} +```{=latex} \begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}, >=stealth] \definecolor{GreenLine}{HTML}{008F45} \definecolor{GreenL}{HTML}{D4EFDF} @@ -1106,6 +1101,7 @@ Unlike static pruning, which discards samples permanently, active learning maint \end{tikzpicture} ``` +::: \index{Uncertainty Sampling!active learning query strategy} \index{Query-by-Committee!active learning strategy} @@ -1115,7 +1111,7 @@ For practitioners willing to invest more compute, expected model change selects Active learning is particularly valuable in domains where labeling requires expertise. In medical imaging, for instance, an AI system diagnosing diseases from X-rays may be confident on common conditions but uncertain about rarer cases. By focusing human annotation on these ambiguous cases, active learning optimizes the use of expensive expert time while accelerating model improvement. -The economic implications are substantial. In production settings, labeling costs often dwarf compute costs because a specialist's time is far more expensive than GPU hours. These query strategies drive each iteration of the active learning loop in @fig-active-learning-loop, and the *active learning ROI* can exceed 10$\times$, as the following example demonstrates. +The economic implications are substantial. In production settings, labeling costs often dwarf compute costs because a specialist's time is far more expensive than GPU hours. These query strategies drive each iteration of the active learning loop in @fig-active-learning-loop, and the *active learning ROI* can exceed 10 $\times$, as the following example demonstrates. ```{python} #| label: active-learning-roi-calc @@ -1174,7 +1170,7 @@ cost_saving_str = fmt(cost_saving_value / MILLION, precision=2) + " Million" **Scenario A: Naive Labeling** -1. **Cost**: Labeling all 1M scans would cost **\$`{python} cost_all_str`** (10$\times$ over budget). +1. **Cost**: Labeling all 1M scans would cost **\$`{python} cost_all_str`** (10 $\times$ over budget). 2. **Time**: You can only afford to label `{python} n_random_str` random scans. 3. **Result**: Your model misses rare pathologies because they weren't in the random `{python} n_random_pct_str`%. @@ -1182,10 +1178,10 @@ cost_saving_str = fmt(cost_saving_value / MILLION, precision=2) + " Million" 1. **Strategy**: Use an uncertainty-based selection to pick the **`{python} n_active_str`** "hardest" scans for the doctor to label. 2. **Cost**: `{python} n_active_str` $\times$ `{python} cost_per_label_str` = **\$`{python} cost_active_str`**. (`{python} cost_active_pct_str`% under budget). -3. **Training Speed**: With `{python} speedup_str`$\times$ less data, each training epoch is **`{python} speedup_str`$\times$ faster**. +3. **Training Speed**: With `{python} speedup_str` $\times$ less data, each training epoch is **`{python} speedup_str` $\times$ faster**. 4. **Result**: Empirical studies suggest that these `{python} n_active_str` "high-information" samples often achieve higher accuracy than `{python} n_random_str` random samples. -**The Systems Conclusion**: Data Selection is not just a "data trick"; it is a **`{python} speedup_str`$\times$ compute accelerator** and a **\$`{python} cost_saving_str`** cost-saving measure. +**The Systems Conclusion**: Data Selection is not just a "data trick"; it is a **`{python} speedup_str` $\times$ compute accelerator** and a **\$`{python} cost_saving_str`** cost-saving measure. ::: Compare the two curves in @fig-active-learning-multiplier: Active Learning shifts the learning curve to the left, achieving target accuracy with far fewer samples than random selection. The curves are illustrative to highlight the qualitative gap. @@ -1261,12 +1257,12 @@ Consistency regularization[^fn-consistency-reg] takes a different angle by enfor [^fn-consistency-reg]: *Consistency regularization*: Rooted in the *smoothness assumption* in semi-supervised learning: if two inputs $x_1$ and $x_2$ are close in input space, their labels should also be close. Sajjadi et al. (2016) and Laine and Aila (2017, "Temporal Ensembling") formalized this as a training objective: minimize the divergence between a model's predictions on an input and its augmented version. The approach draws on the broader principle of *invariance*—if a perturbation should not change the label, the model should learn to ignore it. This is conceptually distinct from data augmentation (which creates more training examples) because it explicitly enforces prediction consistency as a loss term, even for unlabeled data where the "correct" label is unknown. -[^fn-fixmatch]: **FixMatch**: Introduced by Kihyuk Sohn et al. at NeurIPS 2020, FixMatch elegantly unifies pseudo-labeling and consistency regularization into a single framework. The algorithm applies weak augmentation (flipping, cropping) to generate pseudo-labels, then requires the model to predict those same labels on strongly augmented versions (RandAugment, CTAugment) of the same images. Only high-confidence pseudo-labels (above a threshold, typically 0.95) are used, filtering out unreliable labels. FixMatch achieved 94.93% accuracy on CIFAR-10 with just 250 labels—matching fully supervised performance with 200$\times$ fewer annotations—establishing it as a landmark in label-efficient learning. +[^fn-fixmatch]: **FixMatch**: Introduced by Kihyuk Sohn et al. at NeurIPS 2020, FixMatch elegantly unifies pseudo-labeling and consistency regularization into a single framework. The algorithm applies weak augmentation (flipping, cropping) to generate pseudo-labels, then requires the model to predict those same labels on strongly augmented versions (RandAugment, CTAugment) of the same images. Only high-confidence pseudo-labels (above a threshold, typically 0.95) are used, filtering out unreliable labels. FixMatch achieved 94.93% accuracy on CIFAR-10 with just 250 labels—matching fully supervised performance with 200 $\times$ fewer annotations—establishing it as a landmark in label-efficient learning. \index{Label Propagation!graph-based semi-supervised} Label propagation offers a third paradigm through graph-based reasoning: construct a similarity graph over all samples and propagate labels from labeled nodes to their neighbors. This approach works particularly well when the feature space exhibits clear cluster structure. -The systems trade-off in semi-supervised learning is straightforward: it typically achieves the same accuracy as fully supervised training with 5–10$\times$ fewer labels but requires more compute because training processes both labeled and unlabeled samples. Since labeling costs often dominate compute costs in production settings, this trade-off is usually favorable. The results of *FixMatch on CIFAR-10* illustrate this label efficiency concretely. +The systems trade-off in semi-supervised learning is straightforward: it typically achieves the same accuracy as fully supervised training with 5–10 $\times$ fewer labels but requires more compute because training processes both labeled and unlabeled samples. Since labeling costs often dominate compute costs in production settings, this trade-off is usually favorable. The results of *FixMatch on CIFAR-10* illustrate this label efficiency concretely. ```{python} #| label: fixmatch-calc @@ -1342,28 +1338,28 @@ cifar10_fixmatch_40_labels_str = fmt(cifar10_fixmatch_40_labels, precision=0, co ::: {.callout-example title="FixMatch on CIFAR-10"} **FixMatch** [@sohn2020fixmatch] combines pseudo-labeling with consistency regularization to achieve high label efficiency (@tbl-fixmatch-cifar10). -| **Label Budget** | **Method** | **Accuracy** | **Label Efficiency** | -|:------------------------------------------------------|:-----------------|-------------------------------------:|---------------------------------------------------------------:| -| **`{python} cifar10_full_labels_str` (100%)** | Fully Supervised | `{python} cifar10_full_acc`% | Baseline | -| **`{python} cifar10_fixmatch_4k_labels_str` (8%)** | FixMatch | `{python} cifar10_fixmatch_4k_acc`% | **`{python} cifar10_fixmatch_4k_eff`$\times$ more efficient** | -| **`{python} cifar10_fixmatch_250_labels_str` (0.5%)** | FixMatch | `{python} cifar10_fixmatch_250_acc`% | **`{python} cifar10_fixmatch_250_eff`$\times$ more efficient** | -| **`{python} cifar10_fixmatch_40_labels_str` (0.08%)** | FixMatch | `{python} cifar10_fixmatch_40_acc`% | `{python} cifar10_fixmatch_40_eff`$\times$ more efficient | +| **Label Budget** | **Method** | **Accuracy** | **Label Efficiency** | +|:------------------------------------------------------|:-----------------|-------------------------------------:|----------------------------------------------------------------:| +| **`{python} cifar10_full_labels_str` (100%)** | Fully Supervised | `{python} cifar10_full_acc`% | Baseline | +| **`{python} cifar10_fixmatch_4k_labels_str` (8%)** | FixMatch | `{python} cifar10_fixmatch_4k_acc`% | **`{python} cifar10_fixmatch_4k_eff` $\times$ more efficient** | +| **`{python} cifar10_fixmatch_250_labels_str` (0.5%)** | FixMatch | `{python} cifar10_fixmatch_250_acc`% | **`{python} cifar10_fixmatch_250_eff` $\times$ more efficient** | +| **`{python} cifar10_fixmatch_40_labels_str` (0.08%)** | FixMatch | `{python} cifar10_fixmatch_40_acc`% | `{python} cifar10_fixmatch_40_eff` $\times$ more efficient | -: **FixMatch Label Efficiency on CIFAR-10.** With 250 labels (0.5% of the dataset), FixMatch achieves within 1.2 points of full supervision, demonstrating 200$\times$ label efficiency. {#tbl-fixmatch-cifar10 .striped .hover} +: **FixMatch Label Efficiency on CIFAR-10.** With 250 labels (0.5% of the dataset), FixMatch achieves within 1.2 points of full supervision, demonstrating 200 $\times$ label efficiency. {#tbl-fixmatch-cifar10 .striped .hover} -With only `{python} cifar10_fixmatch_250_labels_str` labeled samples (25 per class), FixMatch achieves `{python} cifar10_fixmatch_250_acc`% accuracy, within `{python} acc_loss_str` points of full supervision using `{python} cifar10_fixmatch_250_eff`$\times$ fewer labels. The technique works by generating pseudo-labels on weakly augmented unlabeled images (only when model confidence exceeds 0.95), then training to predict these labels on strongly augmented versions of the same images. +With only `{python} cifar10_fixmatch_250_labels_str` labeled samples (25 per class), FixMatch achieves `{python} cifar10_fixmatch_250_acc`% accuracy, within `{python} acc_loss_str` points of full supervision using `{python} cifar10_fixmatch_250_eff` $\times$ fewer labels. The technique works by generating pseudo-labels on weakly augmented unlabeled images (only when model confidence exceeds 0.95), then training to predict these labels on strongly augmented versions of the same images. -**The Systems Insight**: Semi-supervised learning trades labeled data for unlabeled data and compute. On CIFAR-10, training FixMatch requires ~`{python} fixmatch_compute_multiplier_str`$\times$ more compute than supervised training (processing 50K unlabeled samples per epoch). When labels cost \$1 each and GPU hours cost \$0.50, the math favors semi-supervised: +**The Systems Insight**: Semi-supervised learning trades labeled data for unlabeled data and compute. On CIFAR-10, training FixMatch requires ~`{python} fixmatch_compute_multiplier_str` $\times$ more compute than supervised training (processing 50K unlabeled samples per epoch). When labels cost \$1 each and GPU hours cost \$0.50, the math favors semi-supervised: - Supervised (`{python} cifar10_fixmatch_4k_labels_str` labels): \$`{python} supervised_label_cost_str` labeling + \$`{python} supervised_compute_cost` compute = **\$`{python} supervised_total_str`** - FixMatch (`{python} cifar10_fixmatch_250_labels_str` labels): \$`{python} fixmatch_label_cost_str` labeling + \$`{python} fixmatch_compute_cost` compute = **\$`{python} fixmatch_total_str`** -An `{python} cost_reduction_str`$\times$ cost reduction for ~`{python} acc_loss_str` points of accuracy loss. +An `{python} cost_reduction_str` $\times$ cost reduction for ~`{python} acc_loss_str` points of accuracy loss. ::: These gains are substantial, but semi-supervised learning is not universally applicable. The technique assumes that unlabeled data comes from the same distribution as labeled data, and it struggles when unlabeled data contains out-of-distribution samples (the model confidently mislabels them), when class imbalance is severe (pseudo-labels amplify majority class bias), or when the labeled set does not cover all classes (preventing label propagation for unseen classes). Always validate on a held-out set with true labels to catch distribution mismatch. -Despite these limitations, semi-supervised learning reduces label requirements by 5–10$\times$ while maintaining accuracy. We have now progressively reduced labeling demands through a clear trajectory: coreset selection and deduplication prune low-value samples before training; curriculum learning optimizes the order of presentation during training; active learning queries only the most informative samples for human annotation; and semi-supervised learning exploits unlabeled data to stretch those annotations further. Each technique has pushed the label requirement lower, but none has eliminated it. This raises a deeper question: do we need *any* task-specific labels at all? What if the structure of data itself---the fact that cat images resemble other cat images, that coherent sentences follow grammatical patterns---could provide the supervision signal? +Despite these limitations, semi-supervised learning reduces label requirements by 5–10 $\times$ while maintaining accuracy. We have now progressively reduced labeling demands through a clear trajectory: coreset selection and deduplication prune low-value samples before training; curriculum learning optimizes the order of presentation during training; active learning queries only the most informative samples for human annotation; and semi-supervised learning exploits unlabeled data to stretch those annotations further. Each technique has pushed the label requirement lower, but none has eliminated it. This raises a deeper question: do we need *any* task-specific labels at all? What if the structure of data itself---the fact that cat images resemble other cat images, that coherent sentences follow grammatical patterns---could provide the supervision signal? ## Self-Supervised Learning {#sec-data-selection-selfsupervised-learning-7518} @@ -1411,7 +1407,7 @@ Understanding *why* self-supervised learning dominates modern ML practice requir | **Train from scratch** | 100K–1M labeled | 100% full training | Task-specific collection | | **Fine-tune foundation model** | 100–1K labeled | 1–5% of full training | Reuse pre-training corpus | -: **Cost Amortization in Foundation Model Fine-Tuning.** Pre-training costs are paid once and amortized across all downstream tasks; fine-tuning costs scale with task count but remain small per task. This asymmetry explains why fine-tuning dominates modern ML: the marginal cost of each new task drops by 20$\times$ or more compared to training from scratch. {#tbl-cost-amortization .striped .hover} +: **Cost Amortization in Foundation Model Fine-Tuning.** Pre-training costs are paid once and amortized across all downstream tasks; fine-tuning costs scale with task count but remain small per task. This asymmetry explains why fine-tuning dominates modern ML: the marginal cost of each new task drops by 20 $\times$ or more compared to training from scratch. {#tbl-cost-amortization .striped .hover} To illustrate this economic transformation, consider a company building ten specialized classifiers for tasks such as fraud detection, content moderation, and medical diagnosis. @@ -1477,7 +1473,7 @@ Training each classifier from scratch would require substantial investment in bo The fine-tuning approach restructures these costs. Pre-training requires a one-time investment of `{python} cost_pretrain_value_str` GPU-hours on unlabeled data, but this cost is paid only once. Fine-tuning each task then requires just `{python} labels_per_task_finetune_str` labels (`{python} label_cost_finetune_total_str` total across all ten tasks) and only `{python} cost_finetune_value_str` GPU-hours of compute. Each task reaches deployment in 1–2 weeks after pre-training completes. -The return on investment is substantial across every dimension: labeling costs drop by **`{python} label_cost_drop_str`$\times$** (from `{python} label_cost_scratch_total_str` to `{python} label_cost_finetune_total_str`), per-task marginal compute decreases by **`{python} marginal_compute_reduction_str`$\times$**, and time to deployment accelerates by **20–50$\times$** per task. +The return on investment is substantial across every dimension: labeling costs drop by **`{python} label_cost_drop_str` $\times$** (from `{python} label_cost_scratch_total_str` to `{python} label_cost_finetune_total_str`), per-task marginal compute decreases by **`{python} marginal_compute_reduction_str` $\times$**, and time to deployment accelerates by **20–50 $\times$** per task. This explains *why* the fine-tuning paradigm dominates production ML. The pre-training cost is high but amortized across many downstream applications, while fine-tuning cost remains low on a per-task basis. @@ -1602,7 +1598,7 @@ Generative pre-training, exemplified by the GPT family of models, scales well wi ### Foundation Model Paradigm {#sec-data-selection-foundation-model-paradigm-f0f1} \index{Foundation Models!definition} -These trade-offs converge on a clear conclusion: from a data selection perspective, self-supervised pre-training represents a **1,000$\times$ or greater multiplier** on the value of labeled data. Instead of labeling millions of task-specific examples, practitioners fine-tune on hundreds or thousands of labeled samples while inheriting knowledge distilled from billions of unlabeled tokens. +These trade-offs converge on a clear conclusion: from a data selection perspective, self-supervised pre-training represents a **1,000 $\times$ or greater multiplier** on the value of labeled data. Instead of labeling millions of task-specific examples, practitioners fine-tune on hundreds or thousands of labeled samples while inheriting knowledge distilled from billions of unlabeled tokens. This multiplicative advantage extends the amortization economics examined in the previous section, creating the *foundation model paradigm*\index{Self-supervised learning!foundation model paradigm}[^fn-foundation-model] [@bommasani2021opportunities] that defines modern ML systems. @@ -1650,7 +1646,7 @@ Rather than hand-designing these augmentation policies, AutoAugment[^fn-autoaugm These learned augmentation policies are particularly effective for resource-constrained models, where overfitting risk is highest. The MobileNet lighthouse illustrates this principle: when model capacity is deliberately reduced for edge deployment, augmentation becomes the primary defense against overfitting. ::: {.callout-lighthouse title="MobileNet and Aggressive Augmentation"} -Our **MobileNet Lighthouse model** (@sec-network-architectures) exemplifies how data augmentation compensates for model capacity constraints. MobileNet's depthwise separable convolutions reduce parameters by 8–9$\times$ compared to standard convolutions, but this efficiency comes at a cost: smaller models are more prone to overfitting on limited data. +Our **MobileNet Lighthouse model** (@sec-network-architectures) exemplifies how data augmentation compensates for model capacity constraints. MobileNet's depthwise separable convolutions reduce parameters by 8–9 $\times$ compared to standard convolutions, but this efficiency comes at a cost: smaller models are more prone to overfitting on limited data. The solution is **aggressive augmentation**. MobileNet training typically uses stronger augmentation than ResNet-50 training, including RandAugment with higher magnitude, more aggressive cropping, and longer training schedules. The augmentation effectively increases dataset diversity without increasing model capacity, allowing MobileNet to achieve near-ResNet accuracy at a fraction of the parameter count. For edge deployment where both data collection and model size are constrained, augmentation is essential rather than optional. ::: @@ -1665,7 +1661,7 @@ Three classes of generative approaches address these needs, each with distinct c ### Bridging the Domain Gap {#sec-data-selection-bridging-domain-gap-100b} -[^fn-stable-diffusion]: **Stable Diffusion**: Released by Stability AI in August 2022, based on the latent diffusion model architecture from Robin Rombach et al. at LMU Munich (CVPR 2022). "Stable" refers to the model's training stability achieved by performing diffusion in a compressed latent space (via a variational autoencoder) rather than in pixel space, which reduces computational cost by 10--50$\times$ compared to pixel-space diffusion models like DALL-E 2. For data synthesis, Stable Diffusion's text-to-image capability enables generating targeted training examples from natural language descriptions (e.g., "a dog on a snowy sidewalk at night"), providing fine-grained control over the synthetic data distribution. The open-source release democratized access to high-quality image generation, making synthetic data augmentation practical for teams without massive compute budgets. +[^fn-stable-diffusion]: **Stable Diffusion**: Released by Stability AI in August 2022, based on the latent diffusion model architecture from Robin Rombach et al. at LMU Munich (CVPR 2022). "Stable" refers to the model's training stability achieved by performing diffusion in a compressed latent space (via a variational autoencoder) rather than in pixel space, which reduces computational cost by 10--50 $\times$ compared to pixel-space diffusion models like DALL-E 2. For data synthesis, Stable Diffusion's text-to-image capability enables generating targeted training examples from natural language descriptions (e.g., "a dog on a snowy sidewalk at night"), providing fine-grained control over the synthetic data distribution. The open-source release democratized access to high-quality image generation, making synthetic data augmentation practical for teams without massive compute budgets. \index{Domain Gap!synthetic vs real data} Synthetic data's greatest limitation is the **domain gap**[^fn-domain-gap]: the statistical difference between generated and real-world data, as illustrated in @fig-domain-gap. A model trained only on synthetic data learns a decision boundary optimized for the wrong distribution, potentially performing well on synthetic test data while failing on real deployment data. @@ -1749,7 +1745,7 @@ The optimal mix depends on simulation fidelity, domain complexity, and the cost **Data Selection Solution Stack**: 1. **Seed Data (500 samples)**: Record 50 speakers $\times$ 10 utterances in controlled conditions -2. **Augmentation (5,000 samples)**: Apply pitch shift, time stretch, speed variation to 10$\times$ the seed data +2. **Augmentation (5,000 samples)**: Apply pitch shift, time stretch, speed variation to 10 $\times$ the seed data 3. **Noise Injection (10,000 samples)**: Mix clean audio with environmental noise (kitchen appliances, HVAC, traffic) sampled from AudioSet 4. **Negative Mining**: Use acoustic similarity to find hard negatives ("Hey Siri", "Hey Google") from public datasets 5. **Simulation (optional)**: Text-to-speech synthesis with diverse voice models @@ -1781,11 +1777,11 @@ The preceding sections examined selection techniques across all three pipeline s @tbl-data-selection summarizes the three-stage optimization pipeline introduced at the beginning of this chapter. -| **Stage** | **When Applied** | **Techniques** | **Typical Gains** | -|:----------------------------|:-----------------|:------------------------------------------------------|--------------------------------------:| -| **1. Static Pruning** | Before training | Coreset Selection, Deduplication, Quality Filtering | 30–50% dataset reduction | -| **2. Dynamic Selection** | During training | Curriculum Learning, Active Learning, Semi-Supervised | 10–30% faster convergence | -| **3. Synthetic Generation** | On-demand | Augmentation, Generative Models, Distillation | 2–10$\times$ effective data expansion | +| **Stage** | **When Applied** | **Techniques** | **Typical Gains** | +|:----------------------------|:-----------------|:------------------------------------------------------|---------------------------------------:| +| **1. Static Pruning** | Before training | Coreset Selection, Deduplication, Quality Filtering | 30–50% dataset reduction | +| **2. Dynamic Selection** | During training | Curriculum Learning, Active Learning, Semi-Supervised | 10–30% faster convergence | +| **3. Synthetic Generation** | On-demand | Augmentation, Generative Models, Distillation | 2–10 $\times$ effective data expansion | : **Three-Stage Data Selection Pipeline.** Each stage increases ICR by different mechanisms: pruning removes low-value samples, dynamic selection focuses compute on high-value samples, and synthesis creates new high-value samples. {#tbl-data-selection .striped .hover} @@ -1804,12 +1800,8 @@ The preceding sections examined selection techniques across all three pipeline s @tbl-technique-selection maps individual constraints to techniques, but real projects face multiple constraints simultaneously. The decision tree below (@fig-technique-decision-tree) structures the selection process hierarchically: start by identifying your *primary* bottleneck, then follow the branches to narrow the field. -```{tikz} -#| label: fig-technique-decision-tree -#| echo: false -#| fig-cap: "**Data Selection Technique Selection Tree**: Start at the top by identifying your primary bottleneck, then follow the branches to find the most appropriate technique. Leaf nodes show recommended methods. Multiple paths may apply; combine techniques as needed." -#| fig-alt: "A decision tree flowchart with diamond decision nodes and rectangular technique recommendations. Starts with bottleneck identification and branches to specific techniques." - +::: {#fig-technique-decision-tree fig-cap="**Data Selection Technique Selection Tree**: Start at the top by identifying your primary bottleneck, then follow the branches to find the most appropriate technique. Leaf nodes show recommended methods. Multiple paths may apply; combine techniques as needed." fig-alt="A decision tree flowchart with diamond decision nodes and rectangular technique recommendations. Starts with bottleneck identification and branches to specific techniques."} +```{=latex} \begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}, >=stealth, node distance=1.5cm and 1.5cm] \definecolor{GreenLine}{HTML}{008F45} \definecolor{GreenL}{HTML}{D4EFDF} @@ -1908,6 +1900,7 @@ The preceding sections examined selection techniques across all three pipeline s \end{tikzpicture} ``` +::: The following walkthrough elaborates on each path, guiding practitioners from initial bottleneck identification through implementation. @@ -2129,7 +2122,7 @@ cost_total_proxy_str = fmt(cost_total_proxy, precision=1, commas=False) # e. 2. **Selection (Full Model)**: Scoring the full dataset is equivalent to **`{python} cost_selection_full_str` epoch** of training. T_selection = `{python} cost_selection_full_str` $\times$ C_epoch. 3. **Subset Training**: `{python} n_epochs_full_str` epochs on `{python} subset_fraction_pct_str`% data = `{python} n_epochs_full_str` $\times$ `{python} subset_fraction` $\times$ C_epoch = `{python} n_epochs_subset_str` $\times$ C_epoch. 4. **Total Time**: `{python} cost_selection_full_str` + `{python} n_epochs_subset_str` = **`{python} cost_total_efficient_str`** $\times$ C_epoch. -5. **Speedup**: `{python} n_epochs_full_str` / `{python} cost_total_efficient_str` ≈ **`{python} speedup_efficient_str`$\times$**. +5. **Speedup**: `{python} n_epochs_full_str` / `{python} cost_total_efficient_str` ≈ **`{python} speedup_efficient_str` $\times$**. **The Trap**: If your selection algorithm is iterative (e.g., repeating selection every epoch), T_selection becomes `{python} n_epochs_full_str` $\times$ 1 = `{python} cost_selection_iterative_str` $\times$ C_epoch. Total time = `{python} cost_selection_iterative_str` + `{python} n_epochs_subset_str` = `{python} cost_total_iterative_str` $\times$ C_epoch. You are now **slower** than the baseline. @@ -2210,9 +2203,9 @@ The selection inequality addresses compute overhead, but data selection introduc | **Storage Tier** | **Sequential Throughput** | **Random I/O (IOPS)** | **Random Throughput (approx)** | **Random Penalty** | |:-----------------|--------------------------:|----------------------:|-------------------------------:|-------------------:| -| **HDD (7.2k)** | ~150 MB/s | ~80 | ~0.3 MB/s | **500$\times$** | -| **SATA SSD** | ~550 MB/s | ~10k | ~40 MB/s | **14$\times$** | -| **NVMe SSD** | ~3,500 MB/s | ~500k | ~2,000 MB/s | **1.75$\times$** | +| **HDD (7.2k)** | ~150 MB/s | ~80 | ~0.3 MB/s | **500 $\times$** | +| **SATA SSD** | ~550 MB/s | ~10k | ~40 MB/s | **14 $\times$** | +| **NVMe SSD** | ~3,500 MB/s | ~500k | ~2,000 MB/s | **1.75 $\times$** | | **Cloud (S3)** | ~100 MB/s (per conn) | ~10–50 ms (lat) | Very Low (per conn) | **Extreme** | : **The Cost of Randomness.** Comparative I/O throughput for sequential vs. random 4KB reads across different storage tiers. Standard data loaders optimize for sequential throughput, while data selection strategies often incur the random access penalty. {#tbl-io-performance .striped .hover} @@ -2233,7 +2226,7 @@ The optimizations discussed so far address I/O bandwidth, but modern data select \index{Data Echoing!amortizing I/O costs} Data echoing[^fn-data-echoing] [@choi2020dataechoing] offers an elegant solution to this CPU-GPU imbalance. The technique reuses batches of data multiple times before fetching new samples, effectively trading sample diversity for GPU utilization. When the data pipeline (reading, decoding, augmenting) is slower than GPU processing, the GPU idles waiting for data. -[^fn-data-echoing]: **Data Echoing**: Introduced by Dami Choi et al. at Google Brain (ICML 2020). The technique exploits the observation that in modern training pipelines, the data preprocessing stage (CPU-bound: decoding, resizing, augmenting) is often slower than the training step (GPU-bound: forward pass, backward pass). Rather than letting the GPU idle, data echoing "echoes" (repeats) each preprocessed batch multiple times with different random augmentations, keeping the GPU busy while the CPU prepares fresh data. The key insight is that moderate repetition (2--4$\times$) with varied augmentations degrades convergence minimally compared to the alternative of GPU idling. The paper showed that data echoing after augmentation preserves more than 95% of the convergence benefit of unique samples while nearly eliminating data pipeline stalls. +[^fn-data-echoing]: **Data Echoing**: Introduced by Dami Choi et al. at Google Brain (ICML 2020). The technique exploits the observation that in modern training pipelines, the data preprocessing stage (CPU-bound: decoding, resizing, augmenting) is often slower than the training step (GPU-bound: forward pass, backward pass). Rather than letting the GPU idle, data echoing "echoes" (repeats) each preprocessed batch multiple times with different random augmentations, keeping the GPU busy while the CPU prepares fresh data. The key insight is that moderate repetition (2--4 $\times$) with varied augmentations degrades convergence minimally compared to the alternative of GPU idling. The paper showed that data echoing after augmentation preserves more than 95% of the convergence benefit of unique samples while nearly eliminating data pipeline stalls. Data echoing fills this gap by "echoing" (repeating) each batch $e$ times, applying different augmentations to each repetition so that the model still sees varied inputs. @@ -2321,7 +2314,7 @@ effective_throughput_str = fmt(echo_throughput_value, precision=0, commas=False) - Unique images per second: `{python} pipeline_throughput_str` (unchanged) - Training time: 90 $\times$ 1.28M / `{python} effective_throughput_str` = **`{python} echo_sec_str` seconds (`{python} echo_hrs_str` hours)** if echoed data is equally valuable -**Echoed data has diminishing returns**: Research shows echoed samples provide approximately 70–90% of the value of fresh samples, depending on augmentation diversity. Empirically, Choi et al. measured a **3.25$\times$ speedup** on ResNet-50 ImageNet training when reading data over a network, with minimal accuracy degradation. +**Echoed data has diminishing returns**: Research shows echoed samples provide approximately 70–90% of the value of fresh samples, depending on augmentation diversity. Empirically, Choi et al. measured a **3.25 $\times$ speedup** on ResNet-50 ImageNet training when reading data over a network, with minimal accuracy degradation. **The Trade-Off**: Data echoing trades sample diversity for GPU utilization. It works best when: @@ -2447,7 +2440,7 @@ The challenge lies in quantifying both sides accurately. Different techniques of |:----------------------|:--------------------------------------------------------|:-------------------------------------------------------------| | **Deduplication** | One-time compute for hashing + infrastructure | Reduced storage, fewer epochs for same accuracy | | **Coreset Selection** | Proxy model training + selection compute | Train on 10–50% of data with minimal accuracy loss | -| **Active Learning** | Inference on unlabeled pool + human-in-the-loop latency | 2–10$\times$ reduction in labeling budget for same acc. | +| **Active Learning** | Inference on unlabeled pool + human-in-the-loop latency | 2–10 $\times$ reduction in labeling budget for same acc. | | **Data Augmentation** | CPU/GPU cycles for transforms | Effective dataset size increase without new data acquisition | : **ROI Profiles for Data Selection Techniques.** Each technique occupies a different point in the investment-versus-savings space. Deduplication offers the lowest-risk entry point (minimal investment, guaranteed returns), while active learning offers the highest potential savings but requires the most infrastructure. {#tbl-roi-profiles .striped .hover} @@ -2906,7 +2899,7 @@ $$ \text{DCR} = \frac{N_{\text{full}}}{N_{\text{coreset}}} \text{ at } \text{Accuracy}_{\text{target}} $$ -A DCR of 5$\times$ means the coreset achieves target accuracy with 20% of the data. +A DCR of 5 $\times$ means the coreset achieves target accuracy with 20% of the data. ### The Compute-Optimal Frontier {#sec-data-selection-computeoptimal-frontier-24ba} @@ -2917,7 +2910,7 @@ Research on neural scaling laws [@kaplan2020scaling; @hoffmann2022training] esta \index{Chinchilla!compute-optimal training} The Chinchilla study[^fn-chinchilla] [@hoffmann2022training] revealed a key insight: for any fixed compute budget, there exists an **optimal balance** between model size and training data. Train on too little data relative to model size, and you waste compute on an undertrained model. Train on too much data with too small a model, and you waste data on a model that cannot absorb it. -[^fn-chinchilla]: **Chinchilla**: A 70-billion-parameter language model trained by DeepMind (Hoffmann et al., March 2022), named after the small South American rodent. The paper's central finding upended the "bigger is better" assumption: GPT-3 (175B parameters trained on 300B tokens) was significantly *undertrained* relative to its size. Chinchilla, with only 70B parameters but trained on 1.4T tokens (4.7$\times$ more data), outperformed GPT-3 on most benchmarks. The "Chinchilla scaling law" prescribes that model parameters and training tokens should scale roughly equally: doubling model size should be accompanied by doubling training data. This finding redirected the field's emphasis from model scaling toward data scaling, making data selection techniques directly relevant to frontier model training. +[^fn-chinchilla]: **Chinchilla**: A 70-billion-parameter language model trained by DeepMind (Hoffmann et al., March 2022), named after the small South American rodent. The paper's central finding upended the "bigger is better" assumption: GPT-3 (175B parameters trained on 300B tokens) was significantly *undertrained* relative to its size. Chinchilla, with only 70B parameters but trained on 1.4T tokens (4.7 $\times$ more data), outperformed GPT-3 on most benchmarks. The "Chinchilla scaling law" prescribes that model parameters and training tokens should scale roughly equally: doubling model size should be accompanied by doubling training data. This finding redirected the field's emphasis from model scaling toward data scaling, making data selection techniques directly relevant to frontier model training. This optimal balance defines a **compute-optimal frontier**\index{Compute-optimal frontier!Chinchilla scaling laws}: the best achievable performance at each compute budget when data and model size are properly balanced (@fig-compute-optimal-frontier). @@ -2992,7 +2985,7 @@ For compute-optimal training, the number of training tokens should scale roughly #### Applying the Diagnostic {.unnumbered} -If your training run underperforms expectations, ask: *Am I data-starved or compute-starved?* A simple test: train for 2$\times$ longer. If performance improves substantially, you were compute-starved. If it plateaus quickly, you are data-starved and need better data, not more training. The techniques in this chapter address the data-starved regime; hardware acceleration and distributed training address the compute-starved regime. +If your training run underperforms expectations, ask: *Am I data-starved or compute-starved?* A simple test: train for 2 $\times$ longer. If performance improves substantially, you were compute-starved. If it plateaus quickly, you are data-starved and need better data, not more training. The techniques in this chapter address the data-starved regime; hardware acceleration and distributed training address the compute-starved regime. Watch how the two curves diverge in @fig-ppd-curve: a data-efficient selection strategy (blue) reaches the performance plateau much faster than random sampling (gray). The gap between the curves at any dataset size represents the efficiency opportunity: compute that could be saved by smarter data curation. @@ -3225,7 +3218,7 @@ majority_class_acc_str = fmt(majority_class_acc_value, precision=0, commas=False **Fallacy:** *Data is the new oil, so more is always better.* -Engineers assume linear returns from data scaling: 10$\times$ more data should yield proportional accuracy gains. In reality, the ICR framework (@sec-data-selection-informationcompute-ratio-8c0b) reveals severe diminishing returns. Scaling from `{python} data_1m_str` to `{python} data_10m_str` samples typically yields only `{python} acc_gain_10x_str` percentage points of accuracy gain while incurring 9$\times$ the compute cost. @tbl-scaling-asymmetry quantifies the asymmetry: GPU compute grows 10$\times$ every 3 years while high-quality data grows only 2$\times$ every 5 years. A curated `{python} curated_size_str` dataset achieving `{python} curated_accuracy_str`% accuracy often outperforms a raw `{python} raw_size_str` dataset at `{python} raw_accuracy_str`%, despite `{python} curated_cost_ratio_str`$\times$ fewer samples. Teams that blindly scale data budgets waste compute on redundant samples that contribute near-zero gradient signal. +Engineers assume linear returns from data scaling: 10 $\times$ more data should yield proportional accuracy gains. In reality, the ICR framework (@sec-data-selection-informationcompute-ratio-8c0b) reveals severe diminishing returns. Scaling from `{python} data_1m_str` to `{python} data_10m_str` samples typically yields only `{python} acc_gain_10x_str` percentage points of accuracy gain while incurring 9 $\times$ the compute cost. @tbl-scaling-asymmetry quantifies the asymmetry: GPU compute grows 10 $\times$ every 3 years while high-quality data grows only 2 $\times$ every 5 years. A curated `{python} curated_size_str` dataset achieving `{python} curated_accuracy_str`% accuracy often outperforms a raw `{python} raw_size_str` dataset at `{python} raw_accuracy_str`%, despite `{python} curated_cost_ratio_str` $\times$ fewer samples. Teams that blindly scale data budgets waste compute on redundant samples that contribute near-zero gradient signal. **Fallacy:** *Synthetic data can completely replace real data.* @@ -3233,7 +3226,7 @@ Engineers assume generative models produce unlimited training data at marginal c **Fallacy:** *Data selection is just data cleaning.* -Engineers conflate data quality (removing errors) with data value (maximizing ICR). A perfectly clean dataset can still be highly inefficient if filled with redundant, easy examples far from the decision boundary. @fig-coreset-selection illustrates the distinction: random sampling selects uniformly, wasting budget on samples deep within class regions. Coreset selection (@sec-data-selection-coreset-selection-algorithms-2c74) prioritizes samples near the decision boundary where uncertainty is highest. EL2N and GraNd methods (@tbl-coreset-comparison) achieve 1.8$\times$ higher ICR than random sampling by focusing on informative samples, not just clean ones. Cleaning addresses label errors; selection optimizes information content per FLOP. +Engineers conflate data quality (removing errors) with data value (maximizing ICR). A perfectly clean dataset can still be highly inefficient if filled with redundant, easy examples far from the decision boundary. @fig-coreset-selection illustrates the distinction: random sampling selects uniformly, wasting budget on samples deep within class regions. Coreset selection (@sec-data-selection-coreset-selection-algorithms-2c74) prioritizes samples near the decision boundary where uncertainty is highest. EL2N and GraNd methods (@tbl-coreset-comparison) achieve 1.8 $\times$ higher ICR than random sampling by focusing on informative samples, not just clean ones. Cleaning addresses label errors; selection optimizes information content per FLOP. **Fallacy:** *Data selection is only for resource-constrained settings.* @@ -3243,7 +3236,7 @@ These conceptual misunderstandings often lead to flawed strategies. Equally dama **Pitfall:** *Optimizing selection without measuring selection overhead.* -A sophisticated coreset algorithm requiring `{python} selection_time_bad_str` hours to select samples for a `{python} training_time_str`-hour training run has `{python} selection_overhead_ratio_str`$\times$ overhead, yielding negative ROI. @sec-data-selection-selection-bottleneck-4d00 establishes the Selection Inequality: $T_{selection} + T_{train}(subset) < T_{train}(full)$. If full training takes `{python} full_training_time_str` hours, selection must remain under 10% of that time. Use lightweight proxy models (ResNet-18 for 5 epochs instead of ResNet-50 for 100) or cached embeddings: proxy-based EL2N scoring completes in `{python} selection_time_good_str` minutes (`{python} selection_overhead_good_pct_str`% overhead), satisfying the inequality while achieving comparable selection quality. +A sophisticated coreset algorithm requiring `{python} selection_time_bad_str` hours to select samples for a `{python} training_time_str`-hour training run has `{python} selection_overhead_ratio_str` $\times$ overhead, yielding negative ROI. @sec-data-selection-selection-bottleneck-4d00 establishes the Selection Inequality: $T_{selection} + T_{train}(subset) < T_{train}(full)$. If full training takes `{python} full_training_time_str` hours, selection must remain under 10% of that time. Use lightweight proxy models (ResNet-18 for 5 epochs instead of ResNet-50 for 100) or cached embeddings: proxy-based EL2N scoring completes in `{python} selection_time_good_str` minutes (`{python} selection_overhead_good_pct_str`% overhead), satisfying the inequality while achieving comparable selection quality. **Pitfall:** *Pruning rare classes into oblivion.* @@ -3277,7 +3270,7 @@ This reframing transforms how practitioners approach the ML development lifecycl We explored the three-stage optimization pipeline: **Static Pruning** removes redundancy before training through coreset selection and deduplication, **Dynamic Selection** prioritizes informative examples during training through curriculum and active learning, and **Synthetic Generation** creates data where none exists through augmentation, simulation, and distillation. Together, these strategies address the "Data Wall," the structural asymmetry between exponentially growing compute and slowly growing high-quality data. -The self-supervised learning paradigm represents a ceiling of data selection: by eliminating task-specific labels entirely, foundation models achieve 1,000$\times$ multipliers on downstream tasks through cost amortization. This structural transformation from "train from scratch" to "pre-train once, fine-tune many" has become the dominant approach in production ML precisely because of its superior data economics. +The self-supervised learning paradigm represents a ceiling of data selection: by eliminating task-specific labels entirely, foundation models achieve 1,000 $\times$ multipliers on downstream tasks through cost amortization. This structural transformation from "train from scratch" to "pre-train once, fine-tune many" has become the dominant approach in production ML precisely because of its superior data economics. Translating these techniques into production requires systems engineering: the Selection Inequality ($T_{selection} + T_{train}(subset) < T_{train}(full)$) gates every technique, proxy models and shard-based data loaders reconcile selection algorithms with storage hardware, and data echoing maximizes GPU utilization when pipelines become the bottleneck. The cost modeling framework (total data cost, ROI analysis, and break-even thresholds) provides the quantitative tools to evaluate which techniques merit investment for a given workload, while core metrics (PPD, AULC, DCR) and the compute-optimal frontier diagnostic help practitioners determine whether their training is data-starved or compute-starved. @@ -3287,7 +3280,7 @@ Translating these techniques into production requires systems engineering: the S * **Start with deduplication**: It is the only data selection technique with guaranteed zero accuracy penalty and immediate compute savings. Deduplication should precede all other selection methods in any data pipeline. * **The Selection Inequality gates every technique**: $T_{selection} + T_{train}(subset) < T_{train}(full)$. Selection overhead should stay below 10% of training time. Proxy models and cached embeddings keep $T_{selection}$ low; expensive selection algorithms can consume all the savings they promise. * **Dynamic selection adapts the data diet as the model learns**: Curriculum learning (easy-to-hard ordering) and active learning (uncertainty-guided labeling) exploit the insight that the optimal training distribution changes during training, improving convergence speed and label efficiency respectively. -* **Self-supervised pre-training delivers a 1000$\times$ labeled-data multiplier**: Foundation models amortize expensive pre-training across many downstream tasks, reducing per-task label requirements by 100$\times$ and marginal compute by 20$\times$. This cost amortization is strongest when techniques are reused across teams and training runs. +* **Self-supervised pre-training delivers a 1000 $\times$ labeled-data multiplier**: Foundation models amortize expensive pre-training across many downstream tasks, reducing per-task label requirements by 100 $\times$ and marginal compute by 20 $\times$. This cost amortization is strongest when techniques are reused across teams and training runs. * **Synthetic data is a supplement, not a replacement**: Mixing 50–80% synthetic with 20–50% real data typically yields the best results. Pure synthetic training risks model collapse and domain gap degradation. * **Data selection sits at the head of the D·A·M optimization stack**: Savings from data selection are multiplicative with downstream optimizations (model compression, hardware acceleration). Every FLOP eliminated upstream is a FLOP that never needs to be compressed or accelerated. diff --git a/book/quarto/contents/vol1/frameworks/frameworks.qmd b/book/quarto/contents/vol1/frameworks/frameworks.qmd index 282decfb2d..34580e56bf 100644 --- a/book/quarto/contents/vol1/frameworks/frameworks.qmd +++ b/book/quarto/contents/vol1/frameworks/frameworks.qmd @@ -89,7 +89,7 @@ Two lines of code: `model = Transformer(...)` followed by `loss.backward()`. Bet [^fn-chain-rule]: **Chain Rule**: The foundational calculus rule for differentiating composite functions, independently discovered by Leibniz (1676) and formalized by Euler (1748). For a composition $f(g(x))$, the chain rule states $\frac{df}{dx} = \frac{df}{dg} \cdot \frac{dg}{dx}$—the derivative of the whole equals the product of derivatives of each part. This rule is the mathematical engine behind backpropagation: a neural network is a deeply nested composition of functions (layers), and the chain rule decomposes the gradient of the loss with respect to any parameter into a product of local derivatives along the path from output to that parameter. Robert Wengert's 1964 paper showed that this decomposition could be mechanized as a graph algorithm, laying the foundation for automatic differentiation systems in modern frameworks. -The architectures defined in @sec-network-architectures specify *what* computations neural networks perform, but knowing *what* to compute is entirely different from knowing *how* to compute it efficiently. A Transformer's attention mechanism (introduced in @sec-network-architectures) requires coordinating computation across memory hierarchies and accelerator cores in patterns that naive implementations would execute 100$\times$ slower than optimized ones. Implementing these operations from scratch for every model would make deep learning economically infeasible. ML frameworks exist to bridge this gap by translating high-level model definitions into hardware-specific execution plans that extract maximum performance from silicon. +The architectures defined in @sec-network-architectures specify *what* computations neural networks perform, but knowing *what* to compute is entirely different from knowing *how* to compute it efficiently. A Transformer's attention mechanism (introduced in @sec-network-architectures) requires coordinating computation across memory hierarchies and accelerator cores in patterns that naive implementations would execute 100 $\times$ slower than optimized ones. Implementing these operations from scratch for every model would make deep learning economically infeasible. ML frameworks exist to bridge this gap by translating high-level model definitions into hardware-specific execution plans that extract maximum performance from silicon. \index{ML Framework!compiler analogy} A framework is to machine learning *what* a compiler is to traditional programming. A C compiler translates human-readable code into optimized machine instructions, managing register allocation, instruction scheduling, and memory layout. An ML framework translates high-level model definitions into hardware-specific execution plans, managing operator fusion, memory reuse, and device placement. This analogy is more than metaphor: modern frameworks literally include compilers, as we will see throughout this chapter. @@ -208,7 +208,7 @@ All modern frameworks converge on the same three core problems: *how* to execute ## Execution Problem {#sec-ml-frameworks-execution-problem-e1e1} \index{Execution Problem!definition} -Consider two engineers writing the same neural network. The first debugs interactively, printing tensor shapes after each operation, inspecting intermediate values, and stepping through code with `pdb`. The second waits 30 seconds for compilation, then watches the model run 3$\times$ faster with no ability to inspect any intermediate state. Both are correct; they have simply made different choices about the execution problem, the question of whether operations should execute immediately as written or be recorded for later execution. This choice creates a cascade of engineering trade-offs that shape every aspect of framework behavior, from debugging workflows to deployment options to peak hardware utilization. +Consider two engineers writing the same neural network. The first debugs interactively, printing tensor shapes after each operation, inspecting intermediate values, and stepping through code with `pdb`. The second waits 30 seconds for compilation, then watches the model run 3 $\times$ faster with no ability to inspect any intermediate state. Both are correct; they have simply made different choices about the execution problem, the question of whether operations should execute immediately as written or be recorded for later execution. This choice creates a cascade of engineering trade-offs that shape every aspect of framework behavior, from debugging workflows to deployment options to peak hardware utilization. ### Why Execution Strategy Matters: The Memory Wall {#sec-ml-frameworks-execution-strategy-matters-memory-wall-1ce8} @@ -264,7 +264,7 @@ To understand why execution strategy matters so much, consider the **Memory Wall This creates a critical classification: operations are either **compute-bound**\index{Compute-bound operations} (limited by arithmetic throughput, like large matrix multiplications) or **memory-bound**\index{Memory-bound operations} (limited by data movement, like activation functions and normalization). Most individual neural network operation types (activations, normalizations, element-wise operations) are memory-bound, though the large matrix multiplications that dominate total compute time can be compute-bound. -The key optimization for memory-bound operations is **kernel fusion**\index{Kernel Fusion!optimizing memory-bound ops}, combining multiple operations into a single GPU function (called a *kernel*)[^fn-kernel] to avoid intermediate memory traffic. Fusing a sequence of LayerNorm, Dropout, and ReLU into one kernel can yield 5$\times$ speedup by eliminating intermediate writes between operations. FlashAttention[^fn-flashattention-frameworks] fuses the entire attention computation, reducing HBM traffic by 10–20$\times$ and achieving 2–4$\times$ wall-clock speedup. +The key optimization for memory-bound operations is **kernel fusion**\index{Kernel Fusion!optimizing memory-bound ops}, combining multiple operations into a single GPU function (called a *kernel*)[^fn-kernel] to avoid intermediate memory traffic. Fusing a sequence of LayerNorm, Dropout, and ReLU into one kernel can yield 5 $\times$ speedup by eliminating intermediate writes between operations. FlashAttention[^fn-flashattention-frameworks] fuses the entire attention computation, reducing HBM traffic by 10–20 $\times$ and achieving 2–4 $\times$ wall-clock speedup. [^fn-kernel]: **Kernel**: From German "Kern" (core/nucleus), borrowed from operating systems where it denotes the core program with full hardware access. In GPU programming, a kernel is the function that executes in parallel across thousands of threads. The metaphor extends: just as an OS kernel mediates between software and hardware, a GPU kernel is the fundamental unit where algorithms meet silicon. @@ -515,7 +515,7 @@ The dynamic autograd tape enables capabilities impossible with static graphs. Co ##### Systems Implications: Overhead {.unnumbered} -This flexibility comes with performance costs that map directly to the Iron Law (@sec-introduction-iron-law-ml-systems-c32a). Each forward pass rebuilds the autograd tape from scratch, adding Python object creation, reference counting, and node linking overhead to $L_{lat}$ on every iteration. Every operation goes through Python dispatch---function lookup, argument parsing, type checking---costing ~10μs per operation, which becomes significant for models with thousands of operations. Because the graph is built during execution, the framework cannot see across operations to fuse kernels, so each operation launches its own GPU kernel, inflating both $O$ and $D_{vol}$. The autograd tape itself stores references to all intermediate tensors and `Function` nodes, increasing memory consumption by 2–3$\times$ compared to forward-only execution and adding pressure to $D_{vol}$. Together, these costs create a performance ceiling that becomes visible as models grow smaller and dispatch overhead dominates computation. +This flexibility comes with performance costs that map directly to the Iron Law (@sec-introduction-iron-law-ml-systems-c32a). Each forward pass rebuilds the autograd tape from scratch, adding Python object creation, reference counting, and node linking overhead to $L_{lat}$ on every iteration. Every operation goes through Python dispatch---function lookup, argument parsing, type checking---costing ~10μs per operation, which becomes significant for models with thousands of operations. Because the graph is built during execution, the framework cannot see across operations to fuse kernels, so each operation launches its own GPU kernel, inflating both $O$ and $D_{vol}$. The autograd tape itself stores references to all intermediate tensors and `Function` nodes, increasing memory consumption by 2–3 $\times$ compared to forward-only execution and adding pressure to $D_{vol}$. Together, these costs create a performance ceiling that becomes visible as models grow smaller and dispatch overhead dominates computation. For a typical ResNet-50 forward pass, eager execution overhead adds approximately 5--10 ms compared to an optimized compiled version, with the majority spent in Python dispatch and tape construction rather than actual computation. @@ -605,7 +605,7 @@ Because the framework has the complete graph before execution, it can perform op These optimizations map directly to **Iron Law** terms: kernel fusion reduces $D_{vol}$ by eliminating intermediate memory writes, constant folding reduces $O$ by computing values once, memory pre-allocation reduces $L_{lat}$ by avoiding runtime allocation overhead, and dead code elimination reduces both $O$ and $D_{vol}$. \index{XLA (Accelerated Linear Algebra)!definition} -Compilation frameworks like XLA (Accelerated Linear Algebra)\index{XLA (Accelerated Linear Algebra)!graph compilation}[^fn-xla] [@GoogleXLA] take this further, compiling the TensorFlow graph to optimized machine code for specific hardware. For a transformer encoder block, XLA can achieve 1.5–2$\times$ speedup over unoptimized execution through aggressive fusion and hardware-specific code generation. +Compilation frameworks like XLA (Accelerated Linear Algebra)\index{XLA (Accelerated Linear Algebra)!graph compilation}[^fn-xla] [@GoogleXLA] take this further, compiling the TensorFlow graph to optimized machine code for specific hardware. For a transformer encoder block, XLA can achieve 1.5–2 $\times$ speedup over unoptimized execution through aggressive fusion and hardware-specific code generation. ##### Systems Implications {.unnumbered} @@ -875,11 +875,11 @@ The compiler fuses this into one kernel: `FusedAddRelu`. * **Execution:** 1. Launch `Fused` Kernel: `{python} FusionSpeedup.compiled_overhead_str` µs overhead. 2. Read/Write Memory: $2N$ bytes (intermediate result stays in registers). -* **Total Overhead:** `{python} FusionSpeedup.compiled_overhead_str` µs (**`{python} FusionSpeedup.overhead_speedup_str`$\times$ speedup**). -* **Total Memory Traffic:** 2N bytes (**`{python} FusionSpeedup.bw_efficiency_str`$\times$ bandwidth efficiency**). +* **Total Overhead:** `{python} FusionSpeedup.compiled_overhead_str` µs (**`{python} FusionSpeedup.overhead_speedup_str` $\times$ speedup**). +* **Total Memory Traffic:** 2N bytes (**`{python} FusionSpeedup.bw_efficiency_str` $\times$ bandwidth efficiency**). **The Conclusion:** -Compilation is not magic; it is **overhead amortization**. For small, element-wise operations (like LayerNorm, GELU, Add), overhead often exceeds compute time by 10–100$\times$. Fusing them is the only way to utilize the hardware. +Compilation is not magic; it is **overhead amortization**. For small, element-wise operations (like LayerNorm, GELU, Add), overhead often exceeds compute time by 10–100 $\times$. Fusing them is the only way to utilize the hardware. ::: See this tax play out concretely in @fig-python-tax. Notice how eager execution (top) creates "gaps" where the GPU sits idle while Python dispatches the next kernel. The blue compute regions are short; the red dispatch regions are comparatively long. Compilation (bottom) fuses these operations into a single kernel launch, eliminating the gaps entirely so the GPU spends nearly all its time computing rather than waiting. @@ -935,7 +935,7 @@ plt.show() The natural question is: can this fusion happen automatically? PyTorch 2.0's `torch.compile`[^fn-torch-compile] attempts exactly this by capturing eager code and compiling it into fused kernels without requiring users to write custom CUDA.[^fn-cuda-frameworks] -[^fn-torch-compile]: **`torch.compile`**: Introduced in PyTorch 2.0 (March 2023), `torch.compile` represents PyTorch's answer to the eager-vs-graph trade-off that has defined framework design since 2015. Rather than requiring users to rewrite code for a static graph system, `torch.compile` uses Python bytecode interception (via TorchDynamo) to automatically extract computational graphs from existing eager PyTorch code, then compiles them into optimized kernels. The function signature `model = torch.compile(model)` deliberately mirrors Python's `@functools.lru_cache` decorator pattern: a single-line change that transparently optimizes without altering semantics. Typical speedups range from 1.3--2$\times$ for transformer models, with the compilation overhead amortized over subsequent executions via on-disk caching. +[^fn-torch-compile]: **`torch.compile`**: Introduced in PyTorch 2.0 (March 2023), `torch.compile` represents PyTorch's answer to the eager-vs-graph trade-off that has defined framework design since 2015. Rather than requiring users to rewrite code for a static graph system, `torch.compile` uses Python bytecode interception (via TorchDynamo) to automatically extract computational graphs from existing eager PyTorch code, then compiles them into optimized kernels. The function signature `model = torch.compile(model)` deliberately mirrors Python's `@functools.lru_cache` decorator pattern: a single-line change that transparently optimizes without altering semantics. Typical speedups range from 1.3--2 $\times$ for transformer models, with the compilation overhead amortized over subsequent executions via on-disk caching. [^fn-cuda-frameworks]: **CUDA (Compute Unified Device Architecture)**: NVIDIA's parallel computing platform, first released in June 2007, which made GPU programming accessible through a C-like language. For ML frameworks, CUDA serves as the foundational layer between high-level Python operations and GPU silicon. When PyTorch executes `torch.matmul(A, B)`, this call traverses the framework's dispatcher, selects a CUDA kernel implementation (often from cuBLAS), and launches it on the GPU. Each kernel launch incurs 5--20 μs of overhead as the CPU assembles parameters and signals the GPU. The framework's ability to fuse multiple operations into fewer kernel launches—reducing these launch overheads and the associated memory traffic—is why compilation (via `torch.compile` or TensorRT) yields significant speedups over eager execution. @@ -1033,7 +1033,7 @@ This prints each break location and reason: `Graph break in user code at file.py \index{Graph Compilation!compilation modes} As a project matures from prototyping to production, engineers progressively increase compilation aggressiveness. The default mode (`mode='default'`) applies moderate optimization with fast compilation (5--30 seconds for transformer models), making it suitable for development and training where compilation overhead is amortized over many iterations. When deploying an inference server with fixed input shapes, `mode='reduce-overhead'` minimizes Python interpreter overhead by aggressively capturing operations and enabling CUDA graphs that batch kernel launches, improving throughput by 20--40% over the default. For production training that will run for days, `mode='max-autotune'` generates and benchmarks multiple implementation variants for each operation, increasing compilation time (minutes to hours for large models) but improving runtime performance by 10--30%. This progression---default for development, reduce-overhead for inference, max-autotune for long training runs---mirrors the Compilation Continuum principle we formalize below. -The compilation mode controls *how aggressively* to optimize; the backend controls *what target* to optimize for. TorchInductor (the default) generates Triton kernels for CUDA and C++ for CPU, providing the best general-purpose performance for both training and inference. When cross-platform deployment is required, the ONNX Runtime backend\index{ONNX Runtime!compilation backend} exports the FX graph to ONNX format, enabling execution on CPUs, GPUs, mobile, and edge devices---though limited ONNX operation coverage may cause more graph breaks. For maximum inference throughput on NVIDIA GPUs, the TensorRT backend\index{TensorRT!inference compiler} compiles to NVIDIA's inference engine with aggressive int8 quantization, layer fusion, and kernel autotuning, often achieving 1.5–2$\times$ speedup over TorchInductor. The trade-off is clear: each backend narrows the target to unlock deeper optimization, echoing the flexibility-versus-performance axis that distinguishes eager from graph execution. +The compilation mode controls *how aggressively* to optimize; the backend controls *what target* to optimize for. TorchInductor (the default) generates Triton kernels for CUDA and C++ for CPU, providing the best general-purpose performance for both training and inference. When cross-platform deployment is required, the ONNX Runtime backend\index{ONNX Runtime!compilation backend} exports the FX graph to ONNX format, enabling execution on CPUs, GPUs, mobile, and edge devices---though limited ONNX operation coverage may cause more graph breaks. For maximum inference throughput on NVIDIA GPUs, the TensorRT backend\index{TensorRT!inference compiler} compiles to NVIDIA's inference engine with aggressive int8 quantization, layer fusion, and kernel autotuning, often achieving 1.5–2 $\times$ speedup over TorchInductor. The trade-off is clear: each backend narrows the target to unlock deeper optimization, echoing the flexibility-versus-performance axis that distinguishes eager from graph execution. ##### Practical Example: Measuring Speedup {.unnumbered} @@ -1071,7 +1071,7 @@ for _ in range(100): torch.cuda.synchronize() compiled_time = time.time() - start -print(f"Speedup: {eager_time/compiled_time:.2f}×") +print(f"Speedup: {eager_time/compiled_time:.2f} $\times$ ") # Typical: 2-5x speedup for matrix operations ``` ::: @@ -1159,7 +1159,7 @@ Where: | **ViT-B/16** | 620 | 950 | 1,650 | 25--45 | | **GPT-2 (124M)** | 180 | 260 | 420 | 45--90 | -: **Training and Inference Throughput.** Representative throughput comparison across execution modes for common model architectures on NVIDIA A100 GPU with batch size 32. torch.compile typically provides 1.4 to 1.5$\times$ speedup over eager mode, while TensorRT provides 2 to 3$\times$ speedup but requires longer compilation and is inference only. Compile times vary based on model complexity and optimization level. {#tbl-training-benchmark} +: **Training and Inference Throughput.** Representative throughput comparison across execution modes for common model architectures on NVIDIA A100 GPU with batch size 32. torch.compile typically provides 1.4 to 1.5 $\times$ speedup over eager mode, while TensorRT provides 2 to 3 $\times$ speedup but requires longer compilation and is inference only. Compile times vary based on model complexity and optimization level. {#tbl-training-benchmark} These throughput differences across execution modes raise a practical question: which *framework execution strategy* best serves each workload archetype? @@ -1180,7 +1180,7 @@ The optimal framework execution strategy depends on which **Iron Law** term domi : **Framework Execution Strategy by Workload.** Recommended execution strategy for each workload archetype, aligned to the dominant Iron Law term. Compute-bound workloads benefit most from compilation, while irregular access patterns favor eager execution. {#tbl-framework-archetype-strategy} -**Key insight**: Compilation benefits scale with how much of your workload is *optimizable*. Compute Beasts (@tbl-training-benchmark: ResNet-50 sees 2.6$\times$ speedup from TensorRT) benefit most. Sparse Scatter workloads gain little because their bottleneck (embedding lookups) is inherently irregular. +**Key insight**: Compilation benefits scale with how much of your workload is *optimizable*. Compute Beasts (@tbl-training-benchmark: ResNet-50 sees 2.6 $\times$ speedup from TensorRT) benefit most. Sparse Scatter workloads gain little because their bottleneck (embedding lookups) is inherently irregular. ::: This principle has concrete implications across three regimes. In *research prototyping* ($N_{\text{dev}} \gg N_{\text{prod}}$), teams should stay eager. If the architecture changes every few minutes, compilation overhead dominates. A 30-second compile time with 10 iterations/hour means 5 minutes lost to compilation per hour, often more than the runtime savings. @@ -1344,7 +1344,7 @@ This cumulative latency creates what is effectively *a dispatch tax* on executio * **Hardware Time**: T_hw ≈ `{python} DispatchTax.dispatch_hw_time_us_value` μs (mostly memory latency). * **Software Overhead**: T_sw ≈ `{python} DispatchTax.dispatch_n_ops_value` ops $\times$ `{python} DispatchTax.dispatch_us_per_op_value` μs/op = `{python} DispatchTax.dispatch_sw_time_str` μs. * **Ratio**: `{python} DispatchTax.dispatch_sw_time_str` / `{python} DispatchTax.dispatch_hw_time_us_value` ≈ **`{python} DispatchTax.dispatch_ratio_small_str`**. -* **Conclusion**: You spend `{python} DispatchTax.dispatch_overhead_pct_str`% of time waiting for Python. Compilation yields **`{python} DispatchTax.dispatch_compilation_speedup_str`$\times$ speedup**. +* **Conclusion**: You spend `{python} DispatchTax.dispatch_overhead_pct_str`% of time waiting for Python. Compilation yields **`{python} DispatchTax.dispatch_compilation_speedup_str` $\times$ speedup**. **Scenario 2: GPT-3 Layer (Compute Bound)** @@ -1394,7 +1394,7 @@ class GPT3Context: # Note: Use GPT3Context.gpt3_params_b_str directly. ``` -The principle's implication is that small models benefit *disproportionately* from compilation. A 100-parameter toy model might see 10$\times$ speedup from torch.compile, while a `{python} GPT3Context.gpt3_params_b_str` B-parameter model sees only 1.3$\times$. This explains why compilation matters most for efficient inference on smaller, deployed models. +The principle's implication is that small models benefit *disproportionately* from compilation. A 100-parameter toy model might see 10 $\times$ speedup from torch.compile, while a `{python} GPT3Context.gpt3_params_b_str` B-parameter model sees only 1.3 $\times$. This explains why compilation matters most for efficient inference on smaller, deployed models. The dispatch tax analysis reveals that small operations are overhead-bound regardless of hardware capability. This observation matters most at the extreme edge of the deployment spectrum, where the entire Python runtime is itself an unacceptable overhead. @@ -1456,7 +1456,7 @@ The differentiation problem asks: how should frameworks compute gradients[^fn-gr Building on the backpropagation[^fn-backprop-frameworks] algorithm introduced in @sec-neural-computation (where we established that gradients flow backward through the computation graph via the chain rule), -[^fn-backprop-frameworks]: **Backpropagation**: The algorithm for computing gradients in neural networks, independently discovered multiple times: Paul Werbos (1974 PhD thesis), David Parker (1985), and Yann LeCun (1985), before Rumelhart, Hinton, and Williams popularized it in their landmark 1986 Nature paper "Learning representations by back-propagating errors." The name describes the direction of computation: gradients are "propagated backward" from the output loss through each layer to the input. From a systems perspective, backpropagation's key cost is memory: the forward pass must store all intermediate activations (the "tape") so the backward pass can compute local gradients at each layer. For a transformer with $N_L$ layers and hidden dimension $d$, this requires $O(N_L \cdot d \cdot N)$ memory for batch size $N$—often exceeding the model weights themselves. This memory cost motivates activation checkpointing (@sec-model-training) and is the primary reason training requires 3--4$\times$ more memory than inference. +[^fn-backprop-frameworks]: **Backpropagation**: The algorithm for computing gradients in neural networks, independently discovered multiple times: Paul Werbos (1974 PhD thesis), David Parker (1985), and Yann LeCun (1985), before Rumelhart, Hinton, and Williams popularized it in their landmark 1986 Nature paper "Learning representations by back-propagating errors." The name describes the direction of computation: gradients are "propagated backward" from the output loss through each layer to the input. From a systems perspective, backpropagation's key cost is memory: the forward pass must store all intermediate activations (the "tape") so the backward pass can compute local gradients at each layer. For a transformer with $N_L$ layers and hidden dimension $d$, this requires $O(N_L \cdot d \cdot N)$ memory for batch size $N$—often exceeding the model weights themselves. This memory cost motivates activation checkpointing (@sec-model-training) and is the primary reason training requires 3--4 $\times$ more memory than inference. this section shifts focus from the mathematics to the systems engineering of differentiation: how frameworks represent computation graphs, manage memory for intermediate values, and orchestrate the backward pass efficiently across accelerators. The framework's role is not to perform calculus but to manage the bookkeeping at scale, which is required for the training algorithms detailed in @sec-model-training. @lst-auto_diff_intro illustrates the core idea with a simple three-operation function: @@ -1484,11 +1484,11 @@ Neural network training universally uses reverse mode (covered next), but forwar \index{Dual Numbers!forward mode AD} Forward mode automatic differentiation computes derivatives alongside the original computation, tracking how changes propagate from input to output. This approach mirrors manual derivative computation, making it intuitive to understand and implement. -Forward mode's memory requirements are its strength: the method stores only the original value, a single derivative value, and temporary results. Memory usage stays constant regardless of computation depth, making forward mode particularly suitable for embedded systems, real-time applications, and memory-bandwidth-limited systems. However, this comes with a computational cost. Forward mode doubles the Ops term (in **Iron Law** terms) for each input parameter whose derivative is requested. For a model with $N$ parameters, forward mode multiplies total computation by $N$, because each parameter requires a separate forward pass. Reverse mode, by contrast, adds a constant factor of approximately 2 to 3$\times$ regardless of $N$. This asymmetry explains why forward mode is never used for training neural networks, where $N$ ranges from millions to hundreds of billions. This combination of computational scaling with input count but constant memory creates a specific niche: forward mode excels in scenarios with few inputs but many outputs, such as sensitivity analysis, feature importance computation, and online learning with single-example updates. +Forward mode's memory requirements are its strength: the method stores only the original value, a single derivative value, and temporary results. Memory usage stays constant regardless of computation depth, making forward mode particularly suitable for embedded systems, real-time applications, and memory-bandwidth-limited systems. However, this comes with a computational cost. Forward mode doubles the Ops term (in **Iron Law** terms) for each input parameter whose derivative is requested. For a model with $N$ parameters, forward mode multiplies total computation by $N$, because each parameter requires a separate forward pass. Reverse mode, by contrast, adds a constant factor of approximately 2 to 3 $\times$ regardless of $N$. This asymmetry explains why forward mode is never used for training neural networks, where $N$ ranges from millions to hundreds of billions. This combination of computational scaling with input count but constant memory creates a specific niche: forward mode excels in scenarios with few inputs but many outputs, such as sensitivity analysis, feature importance computation, and online learning with single-example updates. To see the mechanism concretely, consider computing both the value and derivative of $f(x) = x^2 \sin(x)$. @lst-forward_mode_ad shows how forward mode propagates derivative computations alongside every operation, applying the chain rule and product rule at each step: -::: {#lst-forward_mode_ad lst-cap="**Forward Mode AD**: Propagates derivatives forward through the computation graph, computing one directional derivative per forward pass with 2$\times$ computational overhead."} +::: {#lst-forward_mode_ad lst-cap="**Forward Mode AD**: Propagates derivatives forward through the computation graph, computing one directional derivative per forward pass with 2 $\times$ computational overhead."} ```{.python} def f(x): # Computing both value and derivative # Step 1: x -> x² @@ -1530,7 +1530,7 @@ dresult = ( ``` ::: -The dual number trace demonstrates the 2$\times$ computational overhead per input: every arithmetic operation (multiply, sine, product rule combination) is performed twice, once for the value and once for the derivative. For this single-input function, the overhead is acceptable. For a neural network with $N = 100{,}000{,}000$ parameters, computing all gradients would require 100 million such passes, which is why forward mode is restricted to the few-input applications described above. +The dual number trace demonstrates the 2 $\times$ computational overhead per input: every arithmetic operation (multiply, sine, product rule combination) is performed twice, once for the value and once for the derivative. For this single-input function, the overhead is acceptable. For a neural network with $N = 100{,}000{,}000$ parameters, computing all gradients would require 100 million such passes, which is why forward mode is restricted to the few-input applications described above. Forward mode's strength in single-input analysis becomes its fatal weakness for training. A neural network has one scalar loss but millions of parameters, and forward mode would require a separate pass for each one---an intractable $O(N)$ cost that explains why no production framework uses forward mode for training. Forward mode remains useful for targeted analyses such as sensitivity analysis (how does changing one pixel affect the prediction?) and feature importance (which input dimensions most influence the output?), where the number of inputs of interest is small. @@ -1679,9 +1679,9 @@ checkpoint2 = save_for_backward(activation4) ::: \index{Kernel Fusion!eliminating intermediate allocations} -The second strategy is *operation fusion*\index{Kernel Fusion}[^fn-operation-fusion]. Rather than executing matrix multiplication, bias addition, and ReLU as three separate operations, each writing intermediate results to memory, frameworks fuse them into a single kernel. This eliminates intermediate memory allocations entirely and achieves 2 to 3$\times$ speedup on modern GPUs by keeping data in registers and caches. +The second strategy is *operation fusion*\index{Kernel Fusion}[^fn-operation-fusion]. Rather than executing matrix multiplication, bias addition, and ReLU as three separate operations, each writing intermediate results to memory, frameworks fuse them into a single kernel. This eliminates intermediate memory allocations entirely and achieves 2 to 3 $\times$ speedup on modern GPUs by keeping data in registers and caches. -[^fn-operation-fusion]: **Operation Fusion**: Compiler optimization that combines multiple sequential operations into a single kernel to reduce memory bandwidth and latency. For example, fusing matrix multiplication, bias addition, and ReLU activation can eliminate intermediate memory allocations and achieve 2–3$\times$ speedup on modern GPUs. +[^fn-operation-fusion]: **Operation Fusion**: Compiler optimization that combines multiple sequential operations into a single kernel to reduce memory bandwidth and latency. For example, fusing matrix multiplication, bias addition, and ReLU activation can eliminate intermediate memory allocations and achieve 2–3 $\times$ speedup on modern GPUs. The backward pass itself benefits from hardware-specific optimization. Rather than directly translating the mathematical definition of a convolution gradient into code, frameworks implement specialized backward kernels that exploit memory access patterns and hardware capabilities of modern accelerators [@chetlur2014cudnn]. These optimizations, checkpointing, fusion, and specialized kernels, work together to make training practical for architectures that would otherwise exhaust GPU memory in a single forward pass. @@ -1693,7 +1693,7 @@ Checkpointing, fusion, and specialized kernels solve the systems problems of AD. \index{Autograd Engine!internals} \index{PyTorch!autograd system} -The autograd system is the framework component that solves the differentiation problem described in @sec-ml-frameworks-three-problems-every-framework-must-solve-317d. Three systems principles govern its design: the data structure that enables efficient gradient computation, the memory cost of maintaining that data structure, and the control mechanisms that production systems require. Understanding these principles explains why training consumes 100$\times$ more memory than inference for the same model, and why frameworks provide specific mechanisms to manage that cost. +The autograd system is the framework component that solves the differentiation problem described in @sec-ml-frameworks-three-problems-every-framework-must-solve-317d. Three systems principles govern its design: the data structure that enables efficient gradient computation, the memory cost of maintaining that data structure, and the control mechanisms that production systems require. Understanding these principles explains why training consumes 100 $\times$ more memory than inference for the same model, and why frameworks provide specific mechanisms to manage that cost. ##### Principle 1: The Reverse-Linked Graph Structure { .unnumbered} @@ -1785,7 +1785,7 @@ class ResNetMemory: For a network with $N_L$ layers, the system must save approximately $N_L$ activation tensors, one per layer, for the entire batch. Consider a concrete example: ResNet-50 has `{python} ResNetMemory.resnet_params_m_str` M parameters (~`{python} ResNetMemory.resnet_fp32_mb_str` MB in FP32) and processes batch size 64 with 224 $\times$ 224 images. The memory breakdown reveals the scale of this trade-off. Forward activations alone consume approximately 8--12 GB (varying by implementation and checkpointing strategy). Parameter gradients add another ~`{python} ResNetMemory.resnet_fp32_mb_str` MB (the same size as the parameters themselves), and Adam optimizer state contributes ~`{python} ResNetMemory.resnet_adam_mb_str` MB for its two momentum buffers per parameter. The total training footprint reaches `{python} ResNetMemory.resnet_training_min_gb_str`--`{python} ResNetMemory.resnet_training_max_gb_str` GB, compared to just ~`{python} ResNetMemory.resnet_fp32_mb_str` MB for inference alone. -This `{python} ResNetMemory.resnet_training_ratio_str`$\times$ ratio between training and inference memory quantifies why the Data Movement ($D_{vol}$) term dominates training latency in the **Iron Law**. During training, the framework must write all activations to memory during the forward pass and read them back during the backward pass, doubling the memory traffic compared to inference alone. For a complete derivation of the four-component training memory equation ($M_{total} = M_{weights} + M_{gradients} + M_{optimizer} + M_{activations}$) and worked examples at larger model scales, see @sec-algorithm-foundations-true-cost-training-memory-e54e. +This `{python} ResNetMemory.resnet_training_ratio_str` $\times$ ratio between training and inference memory quantifies why the Data Movement ($D_{vol}$) term dominates training latency in the **Iron Law**. During training, the framework must write all activations to memory during the forward pass and read them back during the backward pass, doubling the memory traffic compared to inference alone. For a complete derivation of the four-component training memory equation ($M_{total} = M_{weights} + M_{gradients} + M_{optimizer} + M_{activations}$) and worked examples at larger model scales, see @sec-algorithm-foundations-true-cost-training-memory-e54e. Frameworks provide two primary mechanisms to manage this trade-off. **Gradient checkpointing**\index{Gradient Checkpointing!recomputation strategy} [@chen2016training] trades recomputation for memory: instead of saving all activations, the framework saves only a subset and recomputes the rest during the backward pass. This typically reduces activation memory by 50--90% at the cost of 20--33% additional compute (with optimal $\sqrt{n}$ checkpoint placement). In Iron Law terms, checkpointing increases the $O$ term (recomputation) to reduce the $D_{vol}$ term (memory traffic). **Tensor detachment** provides a complementary mechanism: calling `.detach()` on a tensor removes it from the computation graph entirely, preventing the framework from saving activations through that path. This is essential for transfer learning, where pretrained layers should not accumulate gradients, and reduces the $D_{vol}$ term by eliminating unnecessary activation storage. @@ -1945,7 +1945,7 @@ These three principles connect directly to the framework's role as a compiler fo #### Mixed-Precision Training Support {#sec-ml-frameworks-mixedprecision-training-support-d31d} \index{Mixed Precision!FP16 vs. FP32 trade-offs} -Mixed precision exploits a hardware asymmetry to improve two Iron Law terms simultaneously: Tensor Cores execute FP16 matrix multiplications at 2$\times$ the throughput of FP32 (increasing effective $O/R_{peak}$), while FP16 activations halve the memory footprint (reducing $D_{vol}$). Improving both terms simultaneously is rare; most optimizations improve one at the expense of the other. +Mixed precision exploits a hardware asymmetry to improve two Iron Law terms simultaneously: Tensor Cores execute FP16 matrix multiplications at 2 $\times$ the throughput of FP32 (increasing effective $O/R_{peak}$), while FP16 activations halve the memory footprint (reducing $D_{vol}$). Improving both terms simultaneously is rare; most optimizations improve one at the expense of the other. Frameworks exploit this through automatic mixed-precision APIs that select reduced precision for compute-intensive operations while maintaining FP32 where numerical stability demands it. Inside these APIs, frameworks automatically apply precision rules: matrix multiplications and convolutions use FP16 for bandwidth efficiency, while numerically sensitive operations like softmax and layer normalization remain in FP32. This selective precision maintains accuracy while achieving speedups on modern GPUs with specialized hardware units. Because FP16 has a narrower dynamic range than FP32, gradients can underflow to zero during backpropagation. Loss scaling addresses this by multiplying the loss by a large factor before the backward pass, then dividing gradients by the same factor afterward. @@ -2144,7 +2144,7 @@ The execution and differentiation problems together enable the training loop: th \index{Abstraction Problem!definition} \index{Hardware Abstraction!framework design} \index{Hardware Abstraction!two dimensions (data, execution)} -The hardware diversity described above is not merely inconvenient; it is architecturally fundamental. A GPU offers 1,000$\times$ the parallelism of a CPU but has different memory semantics. A TPU provides higher throughput but requires static shapes. A microcontroller has kilobytes where a server has gigabytes. The abstraction problem asks: how should frameworks hide this complexity behind a single programming interface while still enabling efficient utilization of each target's unique capabilities? +The hardware diversity described above is not merely inconvenient; it is architecturally fundamental. A GPU offers 1,000 $\times$ the parallelism of a CPU but has different memory semantics. A TPU provides higher throughput but requires static shapes. A microcontroller has kilobytes where a server has gigabytes. The abstraction problem asks: how should frameworks hide this complexity behind a single programming interface while still enabling efficient utilization of each target's unique capabilities? The problem decomposes into two interacting dimensions. The first is *data representation*: how should frameworks represent tensors, parameters, and computational state in ways that work across hardware? The second is *execution mapping*: how should high-level operations translate to hardware-specific implementations? These dimensions are not independent concerns. The way data is represented (memory layout, precision, device placement) directly affects what execution strategies are possible. A tensor stored in row-major format on a GPU requires different kernels than one in column-major format on a CPU. A model quantized to INT8 enables entirely different execution paths than FP32. @@ -2285,7 +2285,7 @@ admin_act_calc_md = AdminTax.admin_act_calc_md ``` ::: {.callout-notebook title="The Administrative Tax"} -The memory breakdown for ResNet-50 in @sec-ml-frameworks-principle-2-memorycompute-tradeoff-19f2 showed a concrete ~`{python} ResNetMemory.resnet_training_ratio_str`$\times$ ratio between training and inference memory. Here we generalize that analysis to reveal the full administrative overhead at billion-parameter scale. +The memory breakdown for ResNet-50 in @sec-ml-frameworks-principle-2-memorycompute-tradeoff-19f2 showed a concrete ~`{python} ResNetMemory.resnet_training_ratio_str` $\times$ ratio between training and inference memory. Here we generalize that analysis to reveal the full administrative overhead at billion-parameter scale. **Problem**: Why does your GPU utilization drop when training small models? @@ -2297,9 +2297,9 @@ The memory breakdown for ResNet-50 in @sec-ml-frameworks-principle-2-memorycompu 4. **Activations**: For a batch size of `{python} admin_batch_str` and a `{python} admin_layers_str`-layer network, you must store every intermediate layer output for the backward pass. $$ \text{Activations} \approx \text{Batch} \times \text{Layers} \times \text{Width}^{2} \times 2 \text{ bytes} $$ - For a `{python} admin_width_str`-width model: `{python} admin_act_calc_md`. (Each layer's activation is a `Width × Width` matrix per sample---appropriate for transformer-style models where intermediate projections scale with hidden dimension squared.) + For a `{python} admin_width_str`-width model: `{python} admin_act_calc_md`. (Each layer's activation is a `Width $\times$ Width` matrix per sample---appropriate for transformer-style models where intermediate projections scale with hidden dimension squared.) -**The Systems Conclusion**: Your `{python} admin_weights_gb_str` GB model has an **"Administrative Tax"** of ~`{python} admin_tax_gb_str` GB (`{python} admin_grads_gb_str` GB gradients + `{python} admin_opt_gb_str` GB optimizer + `{python} admin_act_gb_str` GB activations) before you even process the first batch. During training, **Data Movement** includes saving and retrieving these activations, which is why training is often 3–4$\times$ slower than pure inference. +**The Systems Conclusion**: Your `{python} admin_weights_gb_str` GB model has an **"Administrative Tax"** of ~`{python} admin_tax_gb_str` GB (`{python} admin_grads_gb_str` GB gradients + `{python} admin_opt_gb_str` GB optimizer + `{python} admin_act_gb_str` GB activations) before you even process the first batch. During training, **Data Movement** includes saving and retrieving these activations, which is why training is often 3–4 $\times$ slower than pure inference. ::: #### Tensor Structure and Dimensions {#sec-ml-frameworks-tensor-structure-dimensions-4a14} @@ -2356,7 +2356,7 @@ minimum width=18,minimum height=18](3R){0}; This rank hierarchy maps directly onto ML data. A color image is a rank-3 tensor: height x width x 3 channels (red, green, blue). @fig-tensor-data-structure-b breaks this apart, stacking the three color channels so you can see how a single photograph becomes a three-layer numerical grid. Stacking a batch of $N$ images adds a fourth dimension, producing a rank-4 tensor of shape $[N, 3, H, W]$. Every convolutional layer in a vision model consumes and produces tensors of exactly this shape, which is why the tensor abstraction is so central to framework design. -::: {#fig-tensor-data-structure-b fig-env="figure" fig-pos="htb" fig-cap="**Image as RGB Tensor.** Three stacked grids representing the red, green, and blue color channels of an image, with dimension labels showing width, height, and channel depth forming a rank-3 tensor. *Credit: Niklas Lang [https://towardsdatascience.com/what-are-tensors-in-machine-learning-5671814646ff](https://towardsdatascience.com/what-are-tensors-in-machine-learning-5671814646ff)*." fig-alt="Three stacked 3 × 3 grids in red, green, and blue representing RGB color channels. Dimension labels show width 3 pixels, height 3 pixels, and 3 color channels forming a 3D tensor for image data."} +::: {#fig-tensor-data-structure-b fig-env="figure" fig-pos="htb" fig-cap="**Image as RGB Tensor.** Three stacked grids representing the red, green, and blue color channels of an image, with dimension labels showing width, height, and channel depth forming a rank-3 tensor. *Credit: Niklas Lang [https://towardsdatascience.com/what-are-tensors-in-machine-learning-5671814646ff](https://towardsdatascience.com/what-are-tensors-in-machine-learning-5671814646ff)*." fig-alt="Three stacked 3 $\times$ 3 grids in red, green, and blue representing RGB color channels. Dimension labels show width 3 pixels, height 3 pixels, and 3 color channels forming a 3D tensor for image data."} ```{.tikz} \scalebox{0.7}{% \begin{tikzpicture}[font=\usefont{T1}{phv}{m}{n}\Large] @@ -2409,9 +2409,9 @@ This rank hierarchy maps directly onto ML data. A color image is a rank-3 tensor Framework tensors carry more than raw numbers. Each tensor stores metadata that the runtime uses to validate operations and select fast execution paths: a *shape* tuple (e.g., `[64, 3, 224, 224]` for a batch of images), a *dtype* (float32, float16, int8), and a *device* tag (CPU, cuda:0). A matrix multiplication, for instance, checks shape compatibility at dispatch time and uses the dtype to route to the correct hardware kernel, whether a standard FP32 GEMM or a Tensor Core FP16 path. \index{Memory Layout!stride patterns} -Memory layout implementation introduces distinct challenges in tensor design. While tensors provide an abstraction of multi-dimensional data, physical computer memory remains linear. Stride patterns\index{Tensor!stride patterns}\index{Memory Layout!row-major vs. column-major} address this disparity by creating mappings between multi-dimensional tensor indices and linear memory addresses. These patterns significantly impact computational performance by determining memory access patterns during tensor operations. @fig-tensor-memory-layout makes this concrete with a 2$\times$3 tensor: follow the same six values as they map into two different linear orderings---row-major and column-major---and note how the stride values change to compensate. +Memory layout implementation introduces distinct challenges in tensor design. While tensors provide an abstraction of multi-dimensional data, physical computer memory remains linear. Stride patterns\index{Tensor!stride patterns}\index{Memory Layout!row-major vs. column-major} address this disparity by creating mappings between multi-dimensional tensor indices and linear memory addresses. These patterns significantly impact computational performance by determining memory access patterns during tensor operations. @fig-tensor-memory-layout makes this concrete with a 2 $\times$ 3 tensor: follow the same six values as they map into two different linear orderings---row-major and column-major---and note how the stride values change to compensate. -::: {#fig-tensor-memory-layout fig-env="figure" fig-pos="htb" fig-cap="**Tensor Memory Layout**: A 2$\times$3 tensor can be stored in linear memory using either row-major (C-style) or column-major (Fortran-style) ordering. Strides define the number of elements to skip in each dimension when moving through memory, enabling frameworks to calculate memory addresses for tensor[i,j] as base_address + i$\times$stride[0] + j$\times$stride[1]. The choice of memory layout significantly impacts cache performance and computational efficiency." fig-alt="Left: 2 × 3 tensor grid with values 1-6. Right: two linear arrays showing row-major layout (1,2,3,4,5,6) and column-major layout (1,4,2,5,3,6). Below: stride calculations for row-major [3,1] and column-major [1,2]."} +::: {#fig-tensor-memory-layout fig-env="figure" fig-pos="htb" fig-cap="**Tensor Memory Layout**: A 2 $\times$ 3 tensor can be stored in linear memory using either row-major (C-style) or column-major (Fortran-style) ordering. Strides define the number of elements to skip in each dimension when moving through memory, enabling frameworks to calculate memory addresses for tensor[i,j] as base_address + i $\times$ stride[0] + j $\times$ stride[1]. The choice of memory layout significantly impacts cache performance and computational efficiency." fig-alt="Left: 2 $\times$ 3 tensor grid with values 1-6. Right: two linear arrays showing row-major layout (1,2,3,4,5,6) and column-major layout (1,4,2,5,3,6). Below: stride calculations for row-major [3,1] and column-major [1,2]."} ```{.tikz} \begin{tikzpicture}[font=\footnotesize\usefont{T1}{phv}{m}{n}] % Define colors @@ -2456,7 +2456,7 @@ Element [i,j] offset = i $\times$ stride[0] + j $\times$ stride[1] ``` ::: -These memory layout patterns are crucial for framework performance optimization. Row-major layout (used by NumPy, PyTorch) stores elements row by row, making row-wise operations more cache-friendly. Column-major layout (used by some BLAS libraries) stores elements column by column, optimizing column-wise access patterns. The stride values encode this layout information: in row-major layout for a 2$\times$3 tensor, moving to the next row requires skipping 3 elements (stride[0]=3), while moving to the next column requires skipping 1 element (stride[1]=1). +These memory layout patterns are crucial for framework performance optimization. Row-major layout (used by NumPy, PyTorch) stores elements row by row, making row-wise operations more cache-friendly. Column-major layout (used by some BLAS libraries) stores elements column by column, optimizing column-wise access patterns. The stride values encode this layout information: in row-major layout for a 2 $\times$ 3 tensor, moving to the next row requires skipping 3 elements (stride[0]=3), while moving to the next column requires skipping 1 element (stride[1]=1). These memory layout details have direct performance implications. When a convolution kernel accesses weight values, row-major layout means consecutive weights along the output channel dimension are contiguous in memory---enabling efficient vectorized loads. Column-major layout would scatter those same weights across memory, forcing slower gather operations. Careful alignment of stride patterns with hardware memory hierarchies maximizes cache efficiency and memory throughput, with optimal layouts achieving 80--90% of theoretical memory bandwidth (1.5--3.0 TB/s on modern data-center GPUs like the A100 and H100) compared to suboptimal patterns that may achieve only 20--30% utilization. @@ -2469,7 +2469,7 @@ Neural network training typically requires float32 precision to maintain stable Type conversions between different numeric representations require careful management. Operating on tensors with different types demands explicit conversion rules to preserve numerical correctness. These conversions introduce computational costs and risk precision loss. Frameworks provide type casting capabilities but rely on developers to maintain numerical precision across operations. -Tensors answer the first question---*what is the data?*---by encoding shape, layout, and precision into a single abstraction. But a perfectly shaped tensor on the wrong device, or one that must cross a 60$\times$ bandwidth gap to reach the GPU, can erase every layout optimization. The next question is *where* data lives and *how* it moves. +Tensors answer the first question---*what is the data?*---by encoding shape, layout, and precision into a single abstraction. But a perfectly shaped tensor on the wrong device, or one that must cross a 60 $\times$ bandwidth gap to reach the GPU, can erase every layout optimization. The next question is *where* data lives and *how* it moves. #### Device and Memory Management {#sec-ml-frameworks-device-memory-management-9404} @@ -2559,7 +2559,7 @@ a100_mem_str = DeviceBandwidthHierarchy.a100_mem_str \index{Memory Management!device placement} Tensors and their memory layouts establish *what* the framework computes with. Where that data physically resides, and how it moves between locations, determines whether computation happens at full speed or crawls. -Every tensor resides on a specific device, and cross-device operations incur transfer costs that can dominate execution time. PCIe 4.0 delivers `{python} pcie4_gbs_str` GB/s between CPU and GPU, while HBM2e provides `{python} MemoryWallSpecs.a100_bw_tbs_str` TB/s within the GPU. This bandwidth gap, exceeding 60$\times$, means a single misplaced tensor transfer can erase the entire speedup from GPU acceleration. +Every tensor resides on a specific device, and cross-device operations incur transfer costs that can dominate execution time. PCIe 4.0 delivers `{python} pcie4_gbs_str` GB/s between CPU and GPU, while HBM2e provides `{python} MemoryWallSpecs.a100_bw_tbs_str` TB/s within the GPU. This bandwidth gap, exceeding 60 $\times$, means a single misplaced tensor transfer can erase the entire speedup from GPU acceleration. Why does this matter for framework design? Because the framework must track where every tensor lives and enforce that operations only combine tensors on the same device. When data must move, the framework must decide whether to block execution or overlap the transfer with other work. These decisions, invisible to most users, determine whether a training loop achieves 30% or 80% of theoretical hardware throughput. @@ -2569,16 +2569,16 @@ Three systems principles govern effective device and memory management: understa The cost of moving data between devices varies by orders of magnitude depending on the interconnect.[^fn-nvlink-frameworks] Before examining optimization strategies, we need to understand these costs quantitatively. @tbl-device-transfer-overhead shows transfer times for a 1000 $\times$ 1000 float32 tensor (4 MB)---roughly the size of a typical activation tensor in a moderately sized model. The numbers reveal why careless device placement can erase any speedup from GPU acceleration: -[^fn-nvlink-frameworks]: **NVLink**: NVIDIA's high-bandwidth interconnect for GPU-to-GPU communication (see @sec-hardware-acceleration for architecture details), providing `{python} nvlink_a100_gbs_str` GB/s bidirectional bandwidth (NVLink 3.0 on A100) compared to `{python} pcie4_bidir_gbs_str` GB/s bidirectional for PCIe 4.0 x16. Critical for multi-GPU training where gradient synchronization requires moving gigabytes per iteration. NVSwitch extends NVLink to connect 8 GPUs in a fully-connected topology (DGX systems), enabling all-to-all communication without bottlenecks. The ~10$\times$ bandwidth advantage over PCIe determines whether tensor parallelism is practical for a given model size. +[^fn-nvlink-frameworks]: **NVLink**: NVIDIA's high-bandwidth interconnect for GPU-to-GPU communication (see @sec-hardware-acceleration for architecture details), providing `{python} nvlink_a100_gbs_str` GB/s bidirectional bandwidth (NVLink 3.0 on A100) compared to `{python} pcie4_bidir_gbs_str` GB/s bidirectional for PCIe 4.0 x16. Critical for multi-GPU training where gradient synchronization requires moving gigabytes per iteration. NVSwitch extends NVLink to connect 8 GPUs in a fully-connected topology (DGX systems), enabling all-to-all communication without bottlenecks. The ~10 $\times$ bandwidth advantage over PCIe determines whether tensor parallelism is practical for a given model size. -| **Interconnect** | **Bandwidth** | **Transfer Time** | **Relative to Compute** | -|:-----------------|--------------------------------------------------:|--------------------------------:|:-----------------------------------| -| **PCIe 3.0 x16** | 16 GB/s | 0.25 ms | 10$\times$ slower than GPU compute | -| **PCIe 4.0 x16** | `{python} pcie4_gbs_str` GB/s | `{python} pcie4_4mb_ms_str` ms | 5$\times$ slower than GPU compute | -| **NVLink 3.0** | `{python} nvlink_a100_gbs_str` GB/s bidirectional | `{python} nvlink_4mb_ms_str` ms | Comparable to GPU compute | -| **GPU Memory** | `{python} a100_bw_gbs_str` GB/s | `{python} hbm_4mb_ms_str` ms | Optimal | +| **Interconnect** | **Bandwidth** | **Transfer Time** | **Relative to Compute** | +|:-----------------|--------------------------------------------------:|--------------------------------:|:------------------------------------| +| **PCIe 3.0 x16** | 16 GB/s | 0.25 ms | 10 $\times$ slower than GPU compute | +| **PCIe 4.0 x16** | `{python} pcie4_gbs_str` GB/s | `{python} pcie4_4mb_ms_str` ms | 5 $\times$ slower than GPU compute | +| **NVLink 3.0** | `{python} nvlink_a100_gbs_str` GB/s bidirectional | `{python} nvlink_4mb_ms_str` ms | Comparable to GPU compute | +| **GPU Memory** | `{python} a100_bw_gbs_str` GB/s | `{python} hbm_4mb_ms_str` ms | Optimal | -: **Device Transfer Overhead.** Transfer time for a 4 MB tensor across different interconnects. PCIe bandwidth shown is unidirectional (typical for GPU transfers), with full-duplex operation providing 2$\times$ total bandwidth. NVLink bandwidth is bidirectional (300 GB/s per direction). Transfer times dominate for small operations, making device placement critical for performance. {#tbl-device-transfer-overhead} +: **Device Transfer Overhead.** Transfer time for a 4 MB tensor across different interconnects. PCIe bandwidth shown is unidirectional (typical for GPU transfers), with full-duplex operation providing 2 $\times$ total bandwidth. NVLink bandwidth is bidirectional (300 GB/s per direction). Transfer times dominate for small operations, making device placement critical for performance. {#tbl-device-transfer-overhead} These numbers connect directly to the **Iron Law** of performance. Every cross-device transfer inflates the data movement term ($D_{vol}/BW$) at a fraction of the available on-device bandwidth. A PCIe 4.0 transfer at `{python} pcie4_gbs_str` GB/s means moving a 1 GB activation tensor adds approximately `{python} pcie4_1gb_ms_str` ms to the data movement cost, equivalent to roughly `{python} pcie4_1gb_equiv_ops_str` trillion operations on a GPU delivering `{python} A100BLAS.dense_tflops_str` TFLOPS. For a model forward pass taking 0.5 ms on GPU, transferring inputs and outputs over PCIe 3.0 doubles the total latency. When batches are small or models are lightweight, transfer overhead can exceed computation time entirely. @@ -2761,13 +2761,13 @@ pcie4_gbs_str = DataloaderThroughput.pcie4_gbs_str ``` \index{Data Pipeline!throughput optimization} -Streams and events answer the second question---*where does data live, and how does it move?*---by overlapping transfers with computation so that the GPU rarely stalls on a single tensor. But scheduling alone cannot help if data arrives too slowly in the first place. The third question is *how does data arrive fast enough?* The core systems principle is straightforward: the data pipeline must sustain the accelerator's consumption rate. A GPU processing 1,000 images per second at `{python} img_res`$\times$`{python} img_res` resolution requires approximately `{python} dataloader_mbs_str` MB/s of sustained data throughput. If the pipeline cannot maintain this rate, the accelerator idles and the effective utilization term in the **Iron Law** drops below 1. +Streams and events answer the second question---*where does data live, and how does it move?*---by overlapping transfers with computation so that the GPU rarely stalls on a single tensor. But scheduling alone cannot help if data arrives too slowly in the first place. The third question is *how does data arrive fast enough?* The core systems principle is straightforward: the data pipeline must sustain the accelerator's consumption rate. A GPU processing 1,000 images per second at `{python} img_res` $\times$ `{python} img_res` resolution requires approximately `{python} dataloader_mbs_str` MB/s of sustained data throughput. If the pipeline cannot maintain this rate, the accelerator idles and the effective utilization term in the **Iron Law** drops below 1. Frameworks address this throughput requirement through three mechanisms. The first is *parallel worker processes*: the DataLoader spawns multiple CPU processes, each independently loading and preprocessing samples. Because data loading involves disk I/O and CPU-bound transformations (decoding, augmentation, normalization), a single process cannot saturate a modern GPU. Multiple workers overlap I/O wait times with preprocessing computation, collectively sustaining throughput that no single process could achieve. When `num_workers > 0`, the DataLoader distributes sample indices across workers through a shared queue, and workers push completed samples to a data queue that the main process assembles into batches. The second mechanism is *prefetching*. The `prefetch_factor` parameter (default 2) controls how many batches each worker prepares in advance. With 4 workers and `prefetch_factor=2`, the pipeline maintains 8 batches in flight, ensuring the GPU never stalls waiting for data. While the model processes batch $N$ on the GPU, workers simultaneously load and preprocess batch $N+1$ through $N+8$ on CPUs, effectively hiding data loading latency behind computation. The cost is memory consumption proportional to batch size times prefetch depth. -The third mechanism is *pinned memory for DMA transfers*. The `pin_memory=True` option allocates batch data in page-locked (pinned) host memory rather than pageable memory. Pageable memory can be swapped to disk by the operating system, forcing the CUDA runtime to first copy data to a temporary pinned buffer before initiating the GPU transfer. Pinned memory bypasses this intermediate copy, enabling direct memory access (DMA) transfers where the GPU's memory controller reads directly from host memory while the CPU continues other work. For a batch of 64 images at 224$\times$224$\times$3 in FP32 (`{python} batch_mb_str` MB), pinned memory transfer takes approximately `{python} batch_transfer_ms_str` ms over PCIe 4.0 x16 (`{python} pcie4_gbs_str` GB/s) compared to ~3.0 ms with pageable memory, a 2–3$\times$ speedup. The cost is reduced available system memory, as pinned pages cannot be swapped. +The third mechanism is *pinned memory for DMA transfers*. The `pin_memory=True` option allocates batch data in page-locked (pinned) host memory rather than pageable memory. Pageable memory can be swapped to disk by the operating system, forcing the CUDA runtime to first copy data to a temporary pinned buffer before initiating the GPU transfer. Pinned memory bypasses this intermediate copy, enabling direct memory access (DMA) transfers where the GPU's memory controller reads directly from host memory while the CPU continues other work. For a batch of 64 images at 224 $\times$ 224 $\times$ 3 in FP32 (`{python} batch_mb_str` MB), pinned memory transfer takes approximately `{python} batch_transfer_ms_str` ms over PCIe 4.0 x16 (`{python} pcie4_gbs_str` GB/s) compared to ~3.0 ms with pageable memory, a 2–3 $\times$ speedup. The cost is reduced available system memory, as pinned pages cannot be swapped. These three mechanisms appear together in the DataLoader configuration. Understanding how each parameter connects to the underlying systems principle helps practitioners diagnose data pipeline bottlenecks. @lst-dataloader-throughput shows a typical setup where `num_workers` enables parallel loading, `prefetch_factor` controls pipeline depth, and `pin_memory` enables DMA transfers: @@ -3082,9 +3082,9 @@ resnet_gflops_str = ResNetGFLOPS.resnet_gflops_str \index{GEMM!arithmetic intensity} With hardware abstraction managing the platform-specific details, frameworks build a layer of mathematical operations on top. General Matrix Multiply (GEMM)\index{GEMM!matrix multiplication}\index{Tensor Operations!GEMM} dominates ML computation (see @sec-algorithm-foundations-general-matrix-multiply-gemm-b55d for arithmetic intensity analysis and the roofline implications). The operation C = $\alpha$AB + $\beta$C accounts for the vast majority of arithmetic in neural networks: a single ResNet-50 forward pass performs approximately `{python} resnet_gflops_str` billion floating-point operations, nearly all of which reduce to GEMM. Frameworks optimize GEMM through cache-aware tiling (splitting matrices into blocks that fit in L1/L2 cache), loop unrolling for instruction-level parallelism, and shape-specific kernels. Fully connected layers use standard dense GEMM, while convolutional layers use im2col transformations that reshape input patches into matrix columns, converting convolution into GEMM. -Beyond GEMM, frameworks implement BLAS operations\index{BLAS!vector and matrix operations} (AXPY for vector addition, GEMV for matrix-vector products) and element-wise operations\index{Element-wise Operations!memory bandwidth} (activation functions, normalization). Element-wise operations are individually cheap but collectively expensive due to memory bandwidth. Each operation reads and writes the full tensor, so a sequence of five element-wise operations on a 100 MB tensor moves 1 GB of data. Fusing those five operations into a single kernel reduces memory traffic to 200 MB, a 5$\times$ bandwidth savings that directly translates to faster execution. +Beyond GEMM, frameworks implement BLAS operations\index{BLAS!vector and matrix operations} (AXPY for vector addition, GEMV for matrix-vector products) and element-wise operations\index{Element-wise Operations!memory bandwidth} (activation functions, normalization). Element-wise operations are individually cheap but collectively expensive due to memory bandwidth. Each operation reads and writes the full tensor, so a sequence of five element-wise operations on a 100 MB tensor moves 1 GB of data. Fusing those five operations into a single kernel reduces memory traffic to 200 MB, a 5 $\times$ bandwidth savings that directly translates to faster execution. -Numerical precision adds another dimension. Training in FP32 uses 4 bytes per parameter; quantizing to INT8 reduces this to 1 byte, cutting memory by 4$\times$ and enabling 2–4$\times$ throughput improvements on hardware with INT8 acceleration. Training typically requires FP32 for gradient stability, while inference runs at FP16 or INT8 with minimal accuracy loss. Frameworks maintain separate kernel implementations for each precision format and handle mixed-precision workflows where different layers operate at different bit widths within a single forward pass. +Numerical precision adds another dimension. Training in FP32 uses 4 bytes per parameter; quantizing to INT8 reduces this to 1 byte, cutting memory by 4 $\times$ and enabling 2–4 $\times$ throughput improvements on hardware with INT8 acceleration. Training typically requires FP32 for gradient stability, while inference runs at FP16 or INT8 with minimal accuracy loss. Frameworks maintain separate kernel implementations for each precision format and handle mixed-precision workflows where different layers operate at different bit widths within a single forward pass. #### System-Level Operations {#sec-ml-frameworks-systemlevel-operations-6a1c} @@ -3457,17 +3457,17 @@ The functional constraints that JAX imposes become advantages in specific domain The preceding sections described each framework's design philosophy in qualitative terms: graph-first versus eager-first, stateful versus functional. Design philosophy claims, however, are only meaningful when backed by measurement. @tbl-mlfm-comparison quantifies how the architectural choices of TensorFlow, PyTorch, and JAX translate to system characteristics. When examining this comparison, note particularly the differences in execution mode, compilation optimization potential, and distributed scalability---these dimensions most directly impact production deployment decisions. -| **Aspect** | **TensorFlow** | **PyTorch** | **JAX** | -|:------------------------------|:---------------------------------|:-----------------------|:---------------------------| -| **Graph Type** | Static (1.x), Dynamic (2.x) | Dynamic | Functional transformations | -| **Programming Model** | Imperative (2.x), Symbolic (1.x) | Imperative | Functional | -| **Core Data Structure** | Tensor (mutable) | Tensor (mutable) | Array (immutable) | -| **Execution Mode** | Eager (2.x default), Graph | Eager | Just-in-time compilation | -| **Automatic Differentiation** | Reverse mode | Reverse mode | Forward and Reverse mode | -| **Hardware Acceleration** | CPU, GPU, TPU | CPU, GPU | CPU, GPU, TPU | -| **Compilation Optimization** | XLA: 3–10$\times$ speedup | TorchScript: 2$\times$ | XLA: 3–10$\times$ speedup | -| **Memory Efficiency** | 70--90% (workload dependent) | 70--90% (varies) | 75--95% (with XLA fusion) | -| **Distributed Scalability** | High (1024+ GPUs) | High | Very High (1024+ GPUs) | +| **Aspect** | **TensorFlow** | **PyTorch** | **JAX** | +|:------------------------------|:---------------------------------|:------------------------|:---------------------------| +| **Graph Type** | Static (1.x), Dynamic (2.x) | Dynamic | Functional transformations | +| **Programming Model** | Imperative (2.x), Symbolic (1.x) | Imperative | Functional | +| **Core Data Structure** | Tensor (mutable) | Tensor (mutable) | Array (immutable) | +| **Execution Mode** | Eager (2.x default), Graph | Eager | Just-in-time compilation | +| **Automatic Differentiation** | Reverse mode | Reverse mode | Forward and Reverse mode | +| **Hardware Acceleration** | CPU, GPU, TPU | CPU, GPU | CPU, GPU, TPU | +| **Compilation Optimization** | XLA: 3–10 $\times$ speedup | TorchScript: 2 $\times$ | XLA: 3–10 $\times$ speedup | +| **Memory Efficiency** | 70--90% (workload dependent) | 70--90% (varies) | 75--95% (with XLA fusion) | +| **Distributed Scalability** | High (1024+ GPUs) | High | Very High (1024+ GPUs) | : **Framework Characteristics.** Each column reflects a distinct answer to the three core problems. TensorFlow's static graph roots enable XLA compilation but constrain dynamic control flow; PyTorch's eager default maximizes debugging flexibility but limits ahead-of-time optimization; JAX's functional model enables composable transformations and achieves the highest memory efficiency through XLA fusion. {#tbl-mlfm-comparison} @@ -3532,7 +3532,7 @@ These three implementations solve the same mathematical problem but reveal disti [^fn-pure-function]: **Pure Function**: Has no side effects and always returns the same output for the same inputs. Pure functions enable mathematical reasoning about code behavior and safe program transformations. -[^fn-xla]: **XLA (Accelerated Linear Algebra)**: Google's domain-specific compiler released in March 2017, optimizing tensor operations across CPUs, GPUs, and TPUs. The name emphasizes that linear algebra operations (matrix multiplies, convolutions) dominate ML computation. Achieves 3–10$\times$ speedups through operation fusion and hardware-specific codegen. Now part of OpenXLA (2022), a cross-industry effort including Google, Meta, NVIDIA, and Apple. +[^fn-xla]: **XLA (Accelerated Linear Algebra)**: Google's domain-specific compiler released in March 2017, optimizing tensor operations across CPUs, GPUs, and TPUs. The name emphasizes that linear algebra operations (matrix multiplies, convolutions) dominate ML computation. Achieves 3–10 $\times$ speedups through operation fusion and hardware-specific codegen. Now part of OpenXLA (2022), a cross-industry effort including Google, Meta, NVIDIA, and Apple. [^fn-onnx]: **ONNX (Open Neural Network Exchange)** [@bai2019onnx]: Launched September 2017 by Facebook and Microsoft to solve framework fragmentation. Originally named "Toffee" internally at Facebook, the name change emphasized its role as an exchange format. Became a Linux Foundation project in 2019. Enables training in PyTorch and deploying via TensorFlow Lite or TensorRT without manual conversion. @@ -3545,21 +3545,21 @@ How large are these differences in practice? @tbl-framework-efficiency-matrix co | **Framework** | **Inference** **Latency (ms)** | **Memory** **Usage (MB)** | **Energy** **(mJ/inference)** | **Model Size** **Reduction** | **Hardware** **Utilization (%)** | |:--------------------------|-------------------------------:|--------------------------:|------------------------------:|-----------------------------:|---------------------------------:| | **TensorFlow** | 45 | 2,100 | 850 | None | 35 | -| **TensorFlow Lite** | 12 | 180 | 120 | 4$\times$ (quantized) | 65 | -| **TensorFlow Lite Micro** | 8 | 32 | 45 | 8$\times$ (pruned+quant) | 75 | +| **TensorFlow Lite** | 12 | 180 | 120 | 4 $\times$ (quantized) | 65 | +| **TensorFlow Lite Micro** | 8 | 32 | 45 | 8 $\times$ (pruned+quant) | 75 | | **PyTorch** | 52 | 1,800 | 920 | None | 32 | -| **PyTorch Mobile** | 18 | 220 | 180 | 3$\times$ (quantized) | 58 | -| **ONNX Runtime** | 15 | 340 | 210 | 2$\times$ (optimized) | 72 | -| **TensorRT** | 3 | 450 | 65 | 2$\times$ (precision opt) | 88 | -| **Apache TVM** | 6 | 280 | 95 | 3$\times$ (compiled) | 82 | +| **PyTorch Mobile** | 18 | 220 | 180 | 3 $\times$ (quantized) | 58 | +| **ONNX Runtime** | 15 | 340 | 210 | 2 $\times$ (optimized) | 72 | +| **TensorRT** | 3 | 450 | 65 | 2 $\times$ (precision opt) | 88 | +| **Apache TVM** | 6 | 280 | 95 | 3 $\times$ (compiled) | 82 | : **Framework Efficiency Comparison.** Quantitative comparison of major ML frameworks across efficiency dimensions using ResNet-50 inference on representative hardware (NVIDIA A100 GPU for server frameworks, ARM Cortex-A78 for mobile). Metrics reflect production workloads with accuracy maintained within 1% of baseline. Hardware utilization represents percentage of theoretical peak performance on typical operations. {#tbl-framework-efficiency-matrix} \index{TensorRT!hardware utilization} \index{Apache TVM!ML compiler} -The efficiency data reveals several important patterns. First, specialized inference frameworks (TensorRT, Apache TVM) achieve 10–15$\times$ lower latency than general-purpose training frameworks (PyTorch, TensorFlow) on identical hardware, demonstrating that framework selection has quantitative performance implications beyond qualitative design preferences. Second, mobile-optimized variants (TF Lite, PyTorch Mobile) reduce memory requirements by 10$\times$ compared to their full counterparts while maintaining accuracy within 1% through quantization and graph optimization. Third, hardware utilization varies dramatically: TensorRT achieves 88% GPU utilization through aggressive kernel fusion while vanilla PyTorch achieves only 32%, a 2.75$\times$ efficiency gap that directly translates to cost differences in production deployment. +The efficiency data reveals several important patterns. First, specialized inference frameworks (TensorRT, Apache TVM) achieve 10–15 $\times$ lower latency than general-purpose training frameworks (PyTorch, TensorFlow) on identical hardware, demonstrating that framework selection has quantitative performance implications beyond qualitative design preferences. Second, mobile-optimized variants (TF Lite, PyTorch Mobile) reduce memory requirements by 10 $\times$ compared to their full counterparts while maintaining accuracy within 1% through quantization and graph optimization. Third, hardware utilization varies dramatically: TensorRT achieves 88% GPU utilization through aggressive kernel fusion while vanilla PyTorch achieves only 32%, a 2.75 $\times$ efficiency gap that directly translates to cost differences in production deployment. -These efficiency gaps, significant in the data center, become existential as we move beyond the server room. A 17$\times$ latency difference between PyTorch and TensorRT is an optimization opportunity on a cloud GPU; on a microcontroller with 256 KB of RAM, a framework that requires 1.8 GB of memory simply cannot run at all. The question shifts from "which framework is fastest?" to "which framework fits?" +These efficiency gaps, significant in the data center, become existential as we move beyond the server room. A 17 $\times$ latency difference between PyTorch and TensorRT is an optimization opportunity on a cloud GPU; on a microcontroller with 256 KB of RAM, a framework that requires 1.8 GB of memory simply cannot run at all. The question shifts from "which framework is fastest?" to "which framework fits?" ## Deployment Targets {#sec-ml-frameworks-deployment-targets-13f1} @@ -3732,7 +3732,7 @@ When `h = torch.relu(x @ W1 + b1)` executes, PyTorch's eager execution triggers 1. **Python Dispatch**\index{Python Dispatch!overhead} (~1μs): Python interpreter calls `torch.matmul`, which routes through PyTorch's dispatcher to select the CUDA backend. -2. **Kernel Selection**\index{BLAS Library!kernel selection} (~0.5μs): cuBLAS selects an optimized GEMM kernel based on matrix dimensions (`{python} train_batch`$\times$`{python} train_input` $\times$ `{python} train_input`$\times$`{python} train_hidden`). For these dimensions, it might choose a tiled algorithm optimized for L2 cache. +2. **Kernel Selection**\index{BLAS Library!kernel selection} (~0.5μs): cuBLAS selects an optimized GEMM kernel based on matrix dimensions (`{python} train_batch` $\times$ `{python} train_input` $\times$ `{python} train_input` $\times$ `{python} train_hidden`). For these dimensions, it might choose a tiled algorithm optimized for L2 cache. 3. **Kernel Launch** (~5μs): The selected kernel is queued to the GPU's command buffer. The CPU continues immediately (asynchronous execution). @@ -3836,13 +3836,13 @@ train_ce_ai_str = fmt(train_ce_flops / train_ce_mem_bytes, precision=1, commas=F train_bwd_flops_str = fmt((train_bwd_flops * flop).to(MFLOPs).magnitude, precision=0, commas=False) + "M" ``` -| **Component** | **FLOPs** | **Memory Traffic** | **Arithmetic Intensity** | -|:---------------------------------|-------------------------------------------------------------------:|------------------------------:|-----------------------------:| -| **MatMul (x @ W1)** | 2$\times$32$\times$784$\times$256 = `{python} train_mm1_flops_str` | `{python} train_mm1_mem_str` | `{python} train_mm1_ai_str` | -| **ReLU** | 32$\times$256 = `{python} train_relu_flops_str` | `{python} train_relu_mem_str` | `{python} train_relu_ai_str` | -| **MatMul (h @ W2)** | 2$\times$32$\times$256$\times$10 = `{python} train_mm2_flops_str` | `{python} train_mm2_mem_str` | `{python} train_mm2_ai_str` | -| **Cross-entropy** | ~`{python} train_ce_flops_str` | `{python} train_ce_mem_str` | `{python} train_ce_ai_str` | -| **Backward (2$\times$ forward)** | ~`{python} train_bwd_flops_str` | `{python} train_bwd_mem_str` | `{python} train_bwd_ai_str` | +| **Component** | **FLOPs** | **Memory Traffic** | **Arithmetic Intensity** | +|:----------------------------------|-------------------------------------------------------------------------:|------------------------------:|-----------------------------:| +| **MatMul (x @ W1)** | 2 $\times$ 32 $\times$ 784 $\times$ 256 = `{python} train_mm1_flops_str` | `{python} train_mm1_mem_str` | `{python} train_mm1_ai_str` | +| **ReLU** | 32 $\times$ 256 = `{python} train_relu_flops_str` | `{python} train_relu_mem_str` | `{python} train_relu_ai_str` | +| **MatMul (h @ W2)** | 2 $\times$ 32 $\times$ 256 $\times$ 10 = `{python} train_mm2_flops_str` | `{python} train_mm2_mem_str` | `{python} train_mm2_ai_str` | +| **Cross-entropy** | ~`{python} train_ce_flops_str` | `{python} train_ce_mem_str` | `{python} train_ce_ai_str` | +| **Backward (2 $\times$ forward)** | ~`{python} train_bwd_flops_str` | `{python} train_bwd_mem_str` | `{python} train_bwd_ai_str` | : **Per-Operation Roofline Analysis.** FLOPs, memory traffic, and arithmetic intensity for each operation in a two-layer MLP training step. MatMul operations achieve arithmetic intensity above 1.0 (compute-bound on most hardware), while ReLU and cross-entropy are far below 1.0 (memory-bound), quantifying why kernel fusion targets element-wise operations. {#tbl-training-step-roofline} @@ -3902,7 +3902,7 @@ Total: ~`{python} mnist_total_flops_str` FLOPs, ~`{python} mnist_mem_traffic_str The training step is overhead-bound.\index{Overhead-bound!small model training} For small models, Python dispatch and kernel launch dominate. This explains why: -- `torch.compile` provides 2--3$\times$ speedup by fusing operations and reducing kernel launches +- `torch.compile` provides 2--3 $\times$ speedup by fusing operations and reducing kernel launches - Batch size increases help amortize per-batch overhead - Production training uses much larger models where compute dominates @@ -3931,7 +3931,7 @@ This detailed trace through a single training step demonstrates how deeply the t ## Fallacies and Pitfalls {#sec-ml-frameworks-fallacies-pitfalls-61ef} -Framework selection involves subtle trade-offs where intuitions from conventional software engineering fail. The memory wall, kernel fusion constraints, and deployment target diversity create pitfalls that waste months of engineering effort and cause production systems to miss latency targets by 10$\times$ or more. +Framework selection involves subtle trade-offs where intuitions from conventional software engineering fail. The memory wall, kernel fusion constraints, and deployment target diversity create pitfalls that waste months of engineering effort and cause production systems to miss latency targets by 10 $\times$ or more. ```{python} #| label: framework-gaps-calc @@ -3975,15 +3975,15 @@ memory_ratio_str = fmt(memory_ratio_value, precision=0, commas=False) # **Fallacy:** *"All frameworks provide equivalent performance for the same model architecture."* -Engineers assume that ResNet-50 yields identical performance across frameworks since the mathematics is the same. In production, framework implementation matters enormously. @tbl-framework-efficiency-matrix shows PyTorch achieves `{python} pytorch_ms_str` ms inference at 32% hardware utilization while TensorRT delivers `{python} tensorrt_ms_str` ms at 88% utilization---a **`{python} perf_gap_str`$\times$ performance gap** on identical hardware. The difference arises from kernel fusion depth, graph optimization strategies, and memory access patterns that vary dramatically between frameworks. Organizations that assume equivalence miss latency SLAs and require costly last-minute framework migrations. +Engineers assume that ResNet-50 yields identical performance across frameworks since the mathematics is the same. In production, framework implementation matters enormously. @tbl-framework-efficiency-matrix shows PyTorch achieves `{python} pytorch_ms_str` ms inference at 32% hardware utilization while TensorRT delivers `{python} tensorrt_ms_str` ms at 88% utilization---a **`{python} perf_gap_str` $\times$ performance gap** on identical hardware. The difference arises from kernel fusion depth, graph optimization strategies, and memory access patterns that vary dramatically between frameworks. Organizations that assume equivalence miss latency SLAs and require costly last-minute framework migrations. **Pitfall:** *Choosing frameworks based on popularity rather than project requirements.* -Engineers assume the most popular framework works for any project. In reality, deployment constraints dominate. @tbl-framework-efficiency-matrix shows PyTorch Mobile requires `{python} pytorch_mobile_mb_str` MB memory while TensorFlow Lite Micro runs in `{python} tflite_micro_kb_str` KB---a **`{python} memory_ratio_str`$\times$ difference**. Teams that prototype edge applications with PyTorch face either memory bloat that exceeds device capacity or 2--3 month framework migrations after development completes. Evaluate deployment targets per @sec-ml-frameworks-deployment-targets-13f1 *before* selecting a training framework. +Engineers assume the most popular framework works for any project. In reality, deployment constraints dominate. @tbl-framework-efficiency-matrix shows PyTorch Mobile requires `{python} pytorch_mobile_mb_str` MB memory while TensorFlow Lite Micro runs in `{python} tflite_micro_kb_str` KB---a **`{python} memory_ratio_str` $\times$ difference**. Teams that prototype edge applications with PyTorch face either memory bloat that exceeds device capacity or 2--3 month framework migrations after development completes. Evaluate deployment targets per @sec-ml-frameworks-deployment-targets-13f1 *before* selecting a training framework. **Fallacy:** *"Framework abstractions eliminate the need for systems knowledge."* -Engineers assume high-level APIs handle all optimization automatically. The **Roofline Model** (@sec-machine-foundations-roofline-model-2529) proves otherwise: element-wise operations like ReLU achieve arithmetic intensity of 0.125 FLOPs/byte, utilizing **under 0.1%** of an A100's peak compute regardless of framework sophistication. @sec-ml-frameworks-execution-strategy-matters-memory-wall-1ce8 explains why: memory bandwidth, not compute, is the bottleneck for most operations. Engineers who lack this understanding leave 80--90% of hardware capacity unused, directly translating to 5–10$\times$ higher inference costs at production scale. +Engineers assume high-level APIs handle all optimization automatically. The **Roofline Model** (@sec-machine-foundations-roofline-model-2529) proves otherwise: element-wise operations like ReLU achieve arithmetic intensity of 0.125 FLOPs/byte, utilizing **under 0.1%** of an A100's peak compute regardless of framework sophistication. @sec-ml-frameworks-execution-strategy-matters-memory-wall-1ce8 explains why: memory bandwidth, not compute, is the bottleneck for most operations. Engineers who lack this understanding leave 80--90% of hardware capacity unused, directly translating to 5–10 $\times$ higher inference costs at production scale. **Pitfall:** *Ignoring vendor lock-in from framework-specific formats.* @@ -4074,7 +4074,7 @@ eager_total_str = fmt(eager_total_value, precision=1, commas=False) # compiled_total_str = fmt(compiled_total_value, precision=1, commas=False) # e.g. "304.7" ``` -Engineers assume compilation overhead is a one-time cost that pays off quickly. @tbl-training-benchmark shows torch.compile achieves 48% higher ResNet-50 throughput but incurs 15--60 seconds compilation overhead per graph change. For a `{python} n_images_str`-image experiment with `{python} n_recompilations_str` code changes: Eager completes in `{python} eager_total_str` seconds while Compiled requires `{python} compiled_total_str` seconds (including `{python} n_recompilations_str`$\times$ `{python} compilation_time_s_str` s recompilation overhead). Teams that enable compilation during rapid prototyping waste hours waiting for recompilations that negate any throughput gains. +Engineers assume compilation overhead is a one-time cost that pays off quickly. @tbl-training-benchmark shows torch.compile achieves 48% higher ResNet-50 throughput but incurs 15--60 seconds compilation overhead per graph change. For a `{python} n_images_str`-image experiment with `{python} n_recompilations_str` code changes: Eager completes in `{python} eager_total_str` seconds while Compiled requires `{python} compiled_total_str` seconds (including `{python} n_recompilations_str` $\times$ `{python} compilation_time_s_str` s recompilation overhead). Teams that enable compilation during rapid prototyping waste hours waiting for recompilations that negate any throughput gains. ## Summary {#sec-ml-frameworks-summary-07f0} @@ -4091,10 +4091,10 @@ These problems are interconnected and constrained by the **Iron Law** of perform ::: {.callout-takeaways title="The Layer Between Math and Hardware"} * **Three problems define every framework**: Execution (how to run), differentiation (how to train), and abstraction (how to express). TensorFlow prioritizes abstraction for deployment breadth, PyTorch prioritizes execution for research velocity, and JAX reframes differentiation through composable function transformations. These are infrastructure commitments, not tooling preferences. -* **The memory wall drives optimization**: Compute has grown approximately 1000$\times$ faster than memory bandwidth. Kernel fusion, activation checkpointing, mixed-precision training, and data layout optimizations all target the data movement term ($D_{vol}$) in the Iron Law, not the compute term. +* **The memory wall drives optimization**: Compute has grown approximately 1000 $\times$ faster than memory bandwidth. Kernel fusion, activation checkpointing, mixed-precision training, and data layout optimizations all target the data movement term ($D_{vol}$) in the Iron Law, not the compute term. * **Compilation pays off only at scale**: The Compilation Continuum principle (@eq-compilation-benefit) quantifies when compilation benefits exceed costs. Research prototyping favors eager mode; production training and inference favor progressive compilation from JIT to AOT. The Dispatch Overhead Law (@eq-dispatch-overhead) explains why small models benefit disproportionately. * **The nn.Module pattern is widely adopted**: Automatic parameter discovery, mode-dependent behavior, and hierarchical composition with serialization appear across major frameworks, enabling million-parameter optimization in a single `optimizer.step()` call regardless of API syntax. -* **Framework choice constrains deployment by orders of magnitude**: A 17$\times$ latency gap (PyTorch vs. TensorRT) and 7,040$\times$ memory gap (PyTorch Mobile vs. TFLite Micro) on identical models demonstrate that frameworks are not interchangeable. Deployment target must be evaluated before framework selection. +* **Framework choice constrains deployment by orders of magnitude**: A 17 $\times$ latency gap (PyTorch vs. TensorRT) and 7,040 $\times$ memory gap (PyTorch Mobile vs. TFLite Micro) on identical models demonstrate that frameworks are not interchangeable. Deployment target must be evaluated before framework selection. ::: diff --git a/book/quarto/contents/vol1/frontmatter/about.qmd b/book/quarto/contents/vol1/frontmatter/about.qmd index d35f001647..aff81db75f 100644 --- a/book/quarto/contents/vol1/frontmatter/about.qmd +++ b/book/quarto/contents/vol1/frontmatter/about.qmd @@ -29,7 +29,7 @@ Consider a box of LEGO bricks. The same interlocking pieces build a spaceship, a This is *why* the book teaches enduring principles rather than current tools. Dominant frameworks have shifted from Theano to TensorFlow to PyTorch in under a decade. Hardware has evolved from repurposed graphics processors to purpose-built tensor accelerators, with new architectures emerging every year. Any textbook that teaches today's tools will be obsolete before its next edition. The building blocks endure. -We follow a quantitative methodology throughout. Where possible, we replace qualitative advice ("use a smaller model") with measurable reasoning ("reducing parameters by 4$\times$ decreases inference latency by 2.3$\times$ on this hardware, at a cost of 1.2% accuracy"). Engineering decisions should be grounded in measurement, not intuition. +We follow a quantitative methodology throughout. Where possible, we replace qualitative advice ("use a smaller model") with measurable reasoning ("reducing parameters by 4 $\times$ decreases inference latency by 2.3 $\times$ on this hardware, at a cost of 1.2% accuracy"). Engineering decisions should be grounded in measurement, not intuition. ## What This Book Covers {#sec-book-book-covers-180b} @@ -59,7 +59,7 @@ Throughout the book, a margin figure highlights which layers each chapter addres \end{center} ``` -The data bar alongside the stack is not merely decorative. Its shading reflects how data flows between the active layers: where a layer is strongly relevant, its connection into the data bus is pronounced, and the gradient flows between connection points. This makes visible the "skip connections" that data creates between distant layers---a deliberate departure from traditional systems stacks, where only adjacent layers interact. Consider three chapters that illustrate how the lens shifts: +The data bar alongside the stack is not merely decorative. Its uniform shading reflects how central data is to each chapter's concerns---a separate, orthogonal dimension from the layer intensities. The connecting wires between each layer and the data bar show which layers interact with data in that chapter's context. Consider three chapters that illustrate how the lens shifts: ```{=latex} \begin{center} @@ -78,12 +78,12 @@ The data bar alongside the stack is not merely decorative. Its shading reflects \begin{minipage}[t]{0.30\textwidth} \centering {\sffamily\scriptsize\bfseries Responsible Engr.}\\[4pt] -\mlsysstack{0}{0}{10}{10}{20}{40}{90}{25} +\mlsysstack{0}{0}{10}{10}{20}{40}{90}{15} \end{minipage} \end{center} ``` -In Hardware Acceleration, the gradient concentrates at the bottom: data movement through memory hierarchies dominates, and the upper layers fade. In Training, data flows strongly through the middle of the stack, where models, frameworks, and hardware converge. In Responsible Engineering, the gradient lights up at the top---applications and operations---but reaches down through the data bus to training, because biased training data surfaces as biased applications even though several layers sit between them. The same stack, three different stories, connected by data. +In Hardware Acceleration, the layer intensity concentrates at the bottom of the stack while the data bar is faint---this chapter is about silicon, not data. In Training, the middle layers glow and the data bar is moderately lit, reflecting that training consumes data but is fundamentally about optimization. In Responsible Engineering, the top layers dominate and the data bar carries a moderate tint, because biased training data surfaces as biased applications even though several layers sit between them. The same stack, three different stories, with data as an independent dimension. Each part builds on the previous one. We recommend reading sequentially, though the reading paths below offer alternatives for readers with specific goals. diff --git a/book/quarto/contents/vol1/hw_acceleration/hw_acceleration.qmd b/book/quarto/contents/vol1/hw_acceleration/hw_acceleration.qmd index 06e9bb06d7..f792929eec 100644 --- a/book/quarto/contents/vol1/hw_acceleration/hw_acceleration.qmd +++ b/book/quarto/contents/vol1/hw_acceleration/hw_acceleration.qmd @@ -33,7 +33,7 @@ The central surprise of modern computing is that *arithmetic is nearly free whil ::: {.callout-tip title="Learning Objectives"} -- Explain why systolic arrays and tensor cores achieve 10–100$\times$ better efficiency than general-purpose processors for matrix operations +- Explain why systolic arrays and tensor cores achieve 10–100 $\times$ better efficiency than general-purpose processors for matrix operations - Calculate arithmetic intensity and use the roofline model to determine compute-bound versus memory-bound workloads - Predict performance bottlenecks by quantifying the memory wall: bandwidth limits, energy costs, and cache hierarchy trade-offs - Select appropriate dataflow strategies (weight-stationary, output-stationary, input-stationary) based on workload reuse priorities @@ -76,7 +76,7 @@ Hardware alone, however, cannot achieve these gains. The algorithms must be desi ::: \index{Quantization!INT8 co-design}\index{Structured Pruning!hardware alignment} -Co-design explains *why* the compression techniques introduced in @sec-model-compression deliver real speedups. Quantization from FP32 to INT8 (as described in @sec-model-compression) yields 2–4$\times$ acceleration not because of fewer bits in the abstract, but because accelerators pack 4$\times$ more INT8 operations into the same silicon area. Structured pruning improves performance while unstructured pruning often does not, because structured patterns preserve the regular memory access patterns that hardware can optimize. Throughout this chapter, the physical constraints of silicon will reveal *why* some theoretically promising algorithmic optimizations succeed in practice and others fail. +Co-design explains *why* the compression techniques introduced in @sec-model-compression deliver real speedups. Quantization from FP32 to INT8 (as described in @sec-model-compression) yields 2–4 $\times$ acceleration not because of fewer bits in the abstract, but because accelerators pack 4 $\times$ more INT8 operations into the same silicon area. Structured pruning improves performance while unstructured pruning often does not, because structured patterns preserve the regular memory access patterns that hardware can optimize. Throughout this chapter, the physical constraints of silicon will reveal *why* some theoretically promising algorithmic optimizations succeed in practice and others fail. \index{Iron Law!ML systems performance}\index{Amdahl's Law!acceleration ceiling} Hardware acceleration targets specific terms in the **Iron Law of ML Systems** (@sec-introduction-iron-law-ml-systems-c32a), which decomposes end-to-end time into data volume ($D_{vol}/BW$), computation ($O / R_{peak} \cdot \eta$), and fixed latency ($L_{lat}$). While data selection reduced the total data and model compression reduced the ops per sample, hardware acceleration increases the rate at which those ops execute by maximizing the Throughput and Bandwidth denominators. Yet acceleration has a hard ceiling, established by *Amdahl's Law*[^fn-amdahls-law]. @@ -92,10 +92,10 @@ Hardware acceleration does not speed up the entire system; it *only speeds up th $$ Speedup = \frac{1}{(1 - p) + \frac{p}{S}} $$ {#eq-amdahl} * **$p$ (Parallel Fraction):** The matrix multiplications (typically 90–99% of an ML workload). -* **$S$ (Speedup):** The raw speed advantage of the GPU/TPU over the CPU (typically 100–1,000$\times$). +* **$S$ (Speedup):** The raw speed advantage of the GPU/TPU over the CPU (typically 100–1,000 $\times$). * **$1-p$ (Serial Fraction):** Data loading, Python overhead, and kernel launch latency.\index{Serial Fraction!Amdahl bottleneck}\index{Kernel Launch Latency!serial overhead} -**The Pitfall:** If data loading takes 10% of the time ($p=0.9$), even an **infinite speed** accelerator ($S=\infty$) can only achieve a **10$\times$** total speedup. The "boring" serial part dominates the "exciting" AI part. +**The Pitfall:** If data loading takes 10% of the time ($p=0.9$), even an **infinite speed** accelerator ($S=\infty$) can only achieve a **10 $\times$** total speedup. The "boring" serial part dominates the "exciting" AI part. ::: \index{Acceleration Wall!diminishing returns} @@ -148,7 +148,7 @@ Hardware speedups are capped by sequential bottlenecks. **Amdahl's Reality** -- [ ] **Serial Bottlenecks**: Why does a 1,000$\times$ faster GPU only speed up training by 5$\times$ if data loading is slow? (Because $Speedup \le 1/(1-p)$). +- [ ] **Serial Bottlenecks**: Why does a 1,000 $\times$ faster GPU only speed up training by 5 $\times$ if data loading is slow? (Because $Speedup \le 1/(1-p)$). - [ ] **Workload Variation**: Why does ResNet (compute-bound) scale better than MobileNet (latency-bound)? (ResNet spends more time in parallelizable matrix math).\index{ResNet-50!compute-bound workload}\index{MobileNet!latency-bound workload} ::: @@ -253,27 +253,27 @@ amdahl_gpt2_ceil_str = AmdahlH100.amdahl_gpt2_ceil_str **ResNet-50 inference on NVIDIA H100:** -- H100 delivers S = `{python} hw_speedup_str`$\times$ speedup over CPU for matrix multiply (`{python} h100_tflops_int8` TOPS INT8 vs. ~8 TOPS on baseline CPU without AMX extensions) +- H100 delivers S = `{python} hw_speedup_str` $\times$ speedup over CPU for matrix multiply (`{python} h100_tflops_int8` TOPS INT8 vs. ~8 TOPS on baseline CPU without AMX extensions) - Typical inference has P = `{python} p_resnet_str` (`{python} p_resnet_pct_str`% parallelizable, `{python} serial_resnet_pct_str`% serial: data loading, preprocessing, postprocessing) -Speedup = 1 / ((1-`{python} p_resnet_str`) + `{python} p_resnet_str` / `{python} hw_speedup_str`) = 1 / (`{python} serial_resnet_str` + `{python} p_resnet_per_s_str`) ≈ `{python} amdahl_resnet_str`$\times$ +Speedup = 1 / ((1-`{python} p_resnet_str`) + `{python} p_resnet_str` / `{python} hw_speedup_str`) = 1 / (`{python} serial_resnet_str` + `{python} p_resnet_per_s_str`) ≈ `{python} amdahl_resnet_str` $\times$ -Despite a `{python} hw_speedup_str`$\times$ hardware advantage, total system speedup is only **`{python} amdahl_resnet_round_str`$\times$**. The `{python} serial_resnet_pct_str`% serial fraction caps practical gains. +Despite a `{python} hw_speedup_str` $\times$ hardware advantage, total system speedup is only **`{python} amdahl_resnet_round_str` $\times$**. The `{python} serial_resnet_pct_str`% serial fraction caps practical gains. **Contrast with GPT-2 (autoregressive):** - Same H100, but GPT-2 token generation has P = `{python} p_gpt2_str` (`{python} serial_gpt2_pct_str`% serial: KV-cache updates, sampling, Python overhead) -Speedup = 1 / ((1-`{python} p_gpt2_str`) + `{python} p_gpt2_str` / `{python} hw_speedup_str`) = 1 / (`{python} serial_gpt2_str` + `{python} p_gpt2_per_s_str`) ≈ `{python} amdahl_gpt2_str`$\times$ +Speedup = 1 / ((1-`{python} p_gpt2_str`) + `{python} p_gpt2_str` / `{python} hw_speedup_str`) = 1 / (`{python} serial_gpt2_str` + `{python} p_gpt2_per_s_str`) ≈ `{python} amdahl_gpt2_str` $\times$ -The *Bandwidth Hog* archetype suffers more from serial bottlenecks. Even infinite accelerator speed yields only $1/(1-p)$ = `{python} amdahl_gpt2_ceil_str`$\times$ maximum speedup. This is *why* LLM inference optimization focuses on reducing the serial fraction (batching, speculative decoding) rather than raw hardware speed. +The *Bandwidth Hog* archetype suffers more from serial bottlenecks. Even infinite accelerator speed yields only $1/(1-p)$ = `{python} amdahl_gpt2_ceil_str` $\times$ maximum speedup. This is *why* LLM inference optimization focuses on reducing the serial fraction (batching, speculative decoding) rather than raw hardware speed. ::: -These examples reveal that the critical question for any hardware optimization is not "how fast is the chip?" but rather: *is this workload limited by how fast we can compute, or how fast we can move data?* The answer determines which accelerator to choose, which optimizations matter, and whether a 10$\times$ more powerful chip will actually help. The *roofline model* (introduced formally in @sec-machine-foundations-roofline-model-2529 and applied to AI workloads in @sec-hardware-acceleration-roofline-model-42ff) provides the analytical framework for answering this question. It plots an operation's *arithmetic intensity*[^fn-arithmetic-intensity] — defined as the ratio of floating-point operations to bytes of memory traffic (FLOP/byte) — against hardware capabilities, revealing whether performance is capped by compute or bandwidth. A dense matrix multiplication with high arithmetic intensity benefits from more TFLOPS; a LayerNorm with low arithmetic intensity benefits from more memory bandwidth. ResNet-50's convolutions are compute-bound while GPT-2's attention layers are memory-bound, and this distinction is precisely *why* these architectures require different optimization strategies. +These examples reveal that the critical question for any hardware optimization is not "how fast is the chip?" but rather: *is this workload limited by how fast we can compute, or how fast we can move data?* The answer determines which accelerator to choose, which optimizations matter, and whether a 10 $\times$ more powerful chip will actually help. The *roofline model* (introduced formally in @sec-machine-foundations-roofline-model-2529 and applied to AI workloads in @sec-hardware-acceleration-roofline-model-42ff) provides the analytical framework for answering this question. It plots an operation's *arithmetic intensity*[^fn-arithmetic-intensity] — defined as the ratio of floating-point operations to bytes of memory traffic (FLOP/byte) — against hardware capabilities, revealing whether performance is capped by compute or bandwidth. A dense matrix multiplication with high arithmetic intensity benefits from more TFLOPS; a LayerNorm with low arithmetic intensity benefits from more memory bandwidth. ResNet-50's convolutions are compute-bound while GPT-2's attention layers are memory-bound, and this distinction is precisely *why* these architectures require different optimization strategies. [^fn-arithmetic-intensity]: **Arithmetic Intensity**\index{Arithmetic Intensity!etymology}: Also called *operational intensity*. Formalized as the x-axis of the roofline model by Williams, Waterman, and Patterson [@williams2009roofline]. Expressed in FLOPs per byte of DRAM traffic, it captures a fundamental tension: computation is cheap but data movement is expensive. A dense matrix multiply achieves high arithmetic intensity (many FLOPs per byte loaded); a vector addition achieves low intensity (one FLOP per byte). This single metric determines whether more compute or more bandwidth will improve performance. -With this analytical lens in place, the chapter proceeds through four major topics. First, we trace the historical evolution of domain-specific architectures, from floating-point coprocessors through graphics processors to contemporary AI accelerators. Second, we examine the computational primitives that characterize ML workloads (matrix multiplication, vector operations, and nonlinear activation functions) and analyze how specialized hardware optimizes these operations through innovations such as systolic arrays and tensor cores. Third, we turn to memory hierarchy design, where data movement energy costs exceeding computation costs by more than 100$\times$ make on-chip buffer optimization and high-bandwidth memory interfaces critical. Fourth, the software stack: compiler optimization and runtime system support determine the extent to which theoretical hardware capabilities translate into measurable performance. Throughout, the focus remains on single-machine systems; multi-machine coordination constitutes an advanced topic beyond this scope. +With this analytical lens in place, the chapter proceeds through four major topics. First, we trace the historical evolution of domain-specific architectures, from floating-point coprocessors through graphics processors to contemporary AI accelerators. Second, we examine the computational primitives that characterize ML workloads (matrix multiplication, vector operations, and nonlinear activation functions) and analyze how specialized hardware optimizes these operations through innovations such as systolic arrays and tensor cores. Third, we turn to memory hierarchy design, where data movement energy costs exceeding computation costs by more than 100 $\times$ make on-chip buffer optimization and high-bandwidth memory interfaces critical. Fourth, the software stack: compiler optimization and runtime system support determine the extent to which theoretical hardware capabilities translate into measurable performance. Throughout, the focus remains on single-machine systems; multi-machine coordination constitutes an advanced topic beyond this scope. The Amdahl's Law analysis and roofline framework establish the analytical tools; the rest of the chapter examines the hardware that these tools diagnose. We begin with the question that precedes all architecture: *why* did specialized hardware emerge, and what recurring design patterns does that history reveal? @@ -287,7 +287,7 @@ Modern ML accelerators (GPUs with tensor cores, Google's TPUs[^fn-hwacc-tpu], Ap ::: {.callout-example title="The TPUv1 vs. K80 Efficiency Shock"} **The Comparison**: In 2015, Google deployed its first Tensor Processing Unit (TPUv1)\index{TPU!v1 efficiency shock} and compared it to the dominant GPU of the era, the NVIDIA K80\index{NVIDIA!K80}. -**The Shock**: The TPUv1 was not just slightly faster; it was **15$\times$–30$\times$ faster** on inference workloads and achieved **30$\times$–80$\times$ better performance-per-watt**. +**The Shock**: The TPUv1 was not just slightly faster; it was **15 $\times$–30 $\times$ faster** on inference workloads and achieved **30 $\times$–80 $\times$ better performance-per-watt**. **The Reason**: The K80 was a general-purpose processor (good for graphics, physics, diverse math). The TPU was a **Domain-Specific Architecture (DSA)**\index{Domain-Specific Architecture!definition} built for *one thing*: 8-bit integer matrix multiplication\index{INT8!TPU optimization}. It stripped away caches, branch prediction, and out-of-order execution logic to fill the chip with pure arithmetic units (Systolic Arrays)\index{Systolic Arrays!TPU design}. @@ -305,7 +305,7 @@ Hardware specialization emerges when specific computational patterns become the \index{Floating-Point Unit!precision bottleneck} The first phase, the Precision Bottleneck, occurred when scientific and engineering applications required high-precision decimal math that general-purpose CPUs performed poorly. In the late 1970s, CPUs typically emulated floating-point operations in software, requiring hundreds of cycles for a single multiplication. This scalar inefficiency led to the first major instance of hardware specialization: the mathematics coprocessor. -The Intel 8087\index{Intel 8087!floating-point coprocessor}\index{Floating-Point Unit!history} (1980)[^fn-intel-8087] addressed this bottleneck by offloading arithmetic-intensive tasks to a dedicated unit. By implementing floating-point logic in hardware rather than software emulation, the 8087 achieved up to 100$\times$ performance gains for scientific workloads [@fisher_8087_1981]. This established a core principle: when a specific data type or operation consumes the majority of execution cycles, moving it to specialized silicon provides 10–100$\times$ improvements. +The Intel 8087\index{Intel 8087!floating-point coprocessor}\index{Floating-Point Unit!history} (1980)[^fn-intel-8087] addressed this bottleneck by offloading arithmetic-intensive tasks to a dedicated unit. By implementing floating-point logic in hardware rather than software emulation, the 8087 achieved up to 100 $\times$ performance gains for scientific workloads [@fisher_8087_1981]. This established a core principle: when a specific data type or operation consumes the majority of execution cycles, moving it to specialized silicon provides 10–100 $\times$ improvements. [^fn-intel-8087]: **Intel 8087 Impact**: The 8087 coprocessor transformed scientific computing by providing dedicated hardware for the IEEE 754 floating-point standard. This success established the economic model for hardware specialization: premium pricing for dramatic performance jumps in specific domains (CAD, simulation). @@ -377,7 +377,7 @@ To see this recurring cycle of specialization and integration in action, follow The principles established through floating-point acceleration provided a blueprint for addressing subsequent computational challenges. As computing applications diversified, new computational patterns emerged that exceeded the capabilities of general-purpose processors, and each domain contributed unique insights to hardware acceleration strategies. -Graphics processing emerged as a primary driver of hardware specialization in the 1990s. Early graphics accelerators focused on specific operations like bitmap transfers and polygon filling. NVIDIA's GeForce 256\index{NVIDIA!GeForce 256}\index{GPU!history} in 1999 represented a milestone in specialized computing. The GeForce 256 implemented hardware-accelerated transform and lighting (T&L)\index{Transform and Lighting!hardware acceleration}, moving these computations from CPU to dedicated silicon. While not yet programmable, these Graphics Processing Units (GPUs) demonstrated how fixed-function parallel architectures could efficiently handle data-parallel workloads, achieving 50–100$\times$ speedups in 3D rendering tasks like texture mapping and vertex transformation. The transition to programmable shaders with the GeForce 3 (2001) and unified shader architectures with the GeForce 8 (2006) eventually enabled GPU computing for general-purpose workloads. By 2004, high-end GPUs could process over 100 million polygons per second [@owens2008gpu]. +Graphics processing emerged as a primary driver of hardware specialization in the 1990s. Early graphics accelerators focused on specific operations like bitmap transfers and polygon filling. NVIDIA's GeForce 256\index{NVIDIA!GeForce 256}\index{GPU!history} in 1999 represented a milestone in specialized computing. The GeForce 256 implemented hardware-accelerated transform and lighting (T&L)\index{Transform and Lighting!hardware acceleration}, moving these computations from CPU to dedicated silicon. While not yet programmable, these Graphics Processing Units (GPUs) demonstrated how fixed-function parallel architectures could efficiently handle data-parallel workloads, achieving 50–100 $\times$ speedups in 3D rendering tasks like texture mapping and vertex transformation. The transition to programmable shaders with the GeForce 3 (2001) and unified shader architectures with the GeForce 8 (2006) eventually enabled GPU computing for general-purpose workloads. By 2004, high-end GPUs could process over 100 million polygons per second [@owens2008gpu]. \index{Digital Signal Processing!multiply-accumulate units} Concurrently, Digital Signal Processing (DSP) processors established parallel data path architectures with specialized multiply-accumulate units and circular buffers optimized for filtering and transform operations. Texas Instruments' TMS32010 (1983) demonstrated how domain-specific instruction sets could dramatically improve performance for signal processing applications [@lyons2011understanding]. @@ -458,7 +458,7 @@ ax.legend(loc='lower right', fontsize=8) plt.show() ``` -[^fn-dsa]: **Domain-Specific Architectures (DSA)**: Computing architectures optimized for specific application domains rather than general-purpose computation. Unlike CPUs designed for flexibility, DSAs sacrifice programmability for dramatic efficiency gains. Google's TPU achieves 15–30$\times$ better performance per watt than GPUs for neural networks, while video codecs provide 100–1000$\times$ improvements over software decoding. The 2018 Turing Award recognized this shift as the defining trend in modern computer architecture. +[^fn-dsa]: **Domain-Specific Architectures (DSA)**: Computing architectures optimized for specific application domains rather than general-purpose computation. Unlike CPUs designed for flexibility, DSAs sacrifice programmability for dramatic efficiency gains. Google's TPU achieves 15–30 $\times$ better performance per watt than GPUs for neural networks, while video codecs provide 100–1000 $\times$ improvements over software decoding. The 2018 Turing Award recognized this shift as the defining trend in modern computer architecture. [^fn-moores-law-end]: **Moore's Law**\index{Moore, Gordon!transistor density observation}: Intel co-founder Gordon Moore's 1965 observation that transistor density doubles every 18–24 months. This exponential scaling drove computing progress for decades, enabling everything from smartphones to supercomputers. However, power density constraints and rising manufacturing complexity slowed the historical pace of cost-effective scaling. As advanced-node development costs rose from millions to billions of dollars, the industry shifted toward parallelism and specialization. @@ -530,13 +530,13 @@ plt.show() ``` \index{Moore's Law!slowdown impact} -The "easy" gains from shrinking transistors are gone. To sustain the exponential growth required by AI models (which are growing 4–10$\times$ faster than Moore's Law), we cannot simply wait for the next CPU generation. We must shift to a new curve, one defined not by clock speed but by *architecture*. To understand how we reached this inflection point, we must first examine the mechanics of the scaling laws that once fueled the general-purpose era. +The "easy" gains from shrinking transistors are gone. To sustain the exponential growth required by AI models (which are growing 4–10 $\times$ faster than Moore's Law), we cannot simply wait for the next CPU generation. We must shift to a new curve, one defined not by clock speed but by *architecture*. To understand how we reached this inflection point, we must first examine the mechanics of the scaling laws that once fueled the general-purpose era. Historically, improvements in processor performance depended on semiconductor process scaling and increasing clock speeds. As power density limitations restricted further frequency scaling and transistor miniaturization encountered increasing physical and economic constraints, architects explored alternative approaches to sustain computational growth. The result was a shift toward domain-specific architectures, which dedicate silicon resources to optimize computation for specific application domains, trading flexibility for efficiency. Domain-specific architectures achieve superior performance and energy efficiency through several reinforcing principles. First, they employ customized data paths\index{Data Path!customization} optimized for target application patterns, enabling direct hardware execution of common operations. Matrix multiplication units in AI accelerators, for example, implement **systolic arrays**\index{Systolic Arrays!definition} — grid-like networks of processing elements that rhythmically compute and pass data through neighboring units — tailored for neural network computations. Second, they build specialized memory hierarchies\index{Memory Hierarchy!domain-specific} around domain-specific access patterns and data reuse characteristics, with custom cache configurations\index{Cache!specialized configuration}, prefetching logic\index{Prefetching!accelerator optimization}, and memory controllers tuned for expected workloads. Third, they reduce instruction overhead by implementing domain-specific instruction sets that encode common operation sequences into single instructions, minimizing decode and dispatch complexity. Finally, they provide direct hardware implementation of frequently used operations through dedicated circuit blocks that bypass software interpretation entirely, eliminating instruction processing overhead and maximizing throughput. -Modern smartphones illustrate these principles compellingly. They can decode 4K video at 60 frames per second while consuming only a few watts of power, despite video processing requiring billions of operations per second. This efficiency is achieved through dedicated hardware video codecs that implement industry standards such as H.264/AVC (introduced in 2003) and H.265/HEVC (finalized in 2013) [@sullivan2012overview]. These specialized circuits provide 100–1000$\times$ improvements in both performance and power efficiency compared to software-based decoding on general-purpose processors. +Modern smartphones illustrate these principles compellingly. They can decode 4K video at 60 frames per second while consuming only a few watts of power, despite video processing requiring billions of operations per second. This efficiency is achieved through dedicated hardware video codecs that implement industry standards such as H.264/AVC (introduced in 2003) and H.265/HEVC (finalized in 2013) [@sullivan2012overview]. These specialized circuits provide 100–1000 $\times$ improvements in both performance and power efficiency compared to software-based decoding on general-purpose processors. \index{ASIC!Application-Specific Integrated Circuit} The trend toward specialization continues to accelerate, with new architectures emerging for an expanding range of domains. Genomics processing benefits from custom accelerators that optimize sequence alignment and variant calling, reducing the time required for DNA analysis [@Shang2018GenomicsAccel]. Similarly, blockchain computation has produced application-specific integrated circuits (ASICs)[^fn-asics] optimized for cryptographic hashing, substantially increasing the efficiency of mining operations [@Taylor2017ASICMining]. @@ -598,19 +598,19 @@ Machine learning constitutes a computational domain with unique characteristics Machine learning computational requirements reveal limitations in traditional processors. CPUs reach only `{python} cpu_utilization_min_str`–`{python} cpu_utilization_max_str`% utilization on neural network workloads, delivering approximately `{python} cpu_gflops_str` GFLOPS (billions of floating-point operations per second)[^fn-gflops] while consuming hundreds of watts. This inefficiency results from architectural mismatches: CPUs optimize for single-thread performance and irregular memory access, while neural networks require massive parallelism and predictable data streams. The memory bandwidth[^fn-memory-bandwidth-accelerators] constraint — the data transfer rate between memory and processors — becomes particularly severe: a single neural network layer may require accessing gigabytes of parameters, overwhelming CPU cache hierarchies[^fn-cache-hierarchy] designed for kilobyte-scale working sets. -[^fn-gflops]: **GFLOPS/TOPS Performance Metrics**: GFLOPS (10⁹ floating-point ops/second) measures floating-point throughput; TOPS (10¹² ops/second) typically measures INT8 operations in AI accelerators. The A100 delivers `{python} a100_tflops_fp16` TFLOPS for FP16/BF16 dense Tensor Core operations (`{python} a100_tflops_tf32` TFLOPS for TF32), with approximately 2$\times$ higher throughput when structured sparsity is enabled; Apple A17 NPU achieves `{python} mobile_tops` INT8 TOPS. Real workload performance depends on memory bandwidth, achieving 10–30% of peak on typical ML models. +[^fn-gflops]: **GFLOPS/TOPS Performance Metrics**: GFLOPS (10⁹ floating-point ops/second) measures floating-point throughput; TOPS (10¹² ops/second) typically measures INT8 operations in AI accelerators. The A100 delivers `{python} a100_tflops_fp16` TFLOPS for FP16/BF16 dense Tensor Core operations (`{python} a100_tflops_tf32` TFLOPS for TF32), with approximately 2 $\times$ higher throughput when structured sparsity is enabled; Apple A17 NPU achieves `{python} mobile_tops` INT8 TOPS. Real workload performance depends on memory bandwidth, achieving 10–30% of peak on typical ML models. -[^fn-memory-bandwidth-accelerators]: Memory bandwidth (introduced in @sec-introduction) measures data transfer rate between memory and processors. For accelerator design, the gap between CPU memory (tens to hundreds of GB/s) and HBM (terabytes per second) explains why specialized hardware achieves 10–100$\times$ speedups on bandwidth-bound neural network operations. +[^fn-memory-bandwidth-accelerators]: Memory bandwidth (introduced in @sec-introduction) measures data transfer rate between memory and processors. For accelerator design, the gap between CPU memory (tens to hundreds of GB/s) and HBM (terabytes per second) explains why specialized hardware achieves 10–100 $\times$ speedups on bandwidth-bound neural network operations. [^fn-cache-hierarchy]: **Cache**: From French "cacher" (to hide), describing memory that stores frequently accessed data out of sight from the programmer. The IBM System/360 Model 85 (1968) introduced the first commercial cache. Modern CPUs use multi-level hierarchies (L1/L2/L3) with progressively larger capacity but higher latency. Neural networks often exceed cache capacity, forcing frequent DRAM accesses that reduce effective throughput. \index{Energy!data movement cost} -The energy economics of data movement influence accelerator design. Accessing data from DRAM can consume on the order of \(10^2\)$\times$ more energy than a multiply-accumulate operation (exact values vary by technology node and design), making minimizing data movement a primary optimization target. This disparity helps explain the progression from repurposed graphics processors to purpose-built neural network accelerators. TPUs and other custom accelerators can sustain high utilization on dense kernels by implementing systolic arrays and other architectures that maximize data reuse while minimizing movement. +The energy economics of data movement influence accelerator design. Accessing data from DRAM can consume on the order of \(10^2\) $\times$ more energy than a multiply-accumulate operation (exact values vary by technology node and design), making minimizing data movement a primary optimization target. This disparity helps explain the progression from repurposed graphics processors to purpose-built neural network accelerators. TPUs and other custom accelerators can sustain high utilization on dense kernels by implementing systolic arrays and other architectures that maximize data reuse while minimizing movement. \index{Training vs. Inference!accelerator design} Training and inference present distinct computational profiles that influence accelerator design. Training requires high-precision arithmetic (FP32 or FP16)\index{FP16!training precision}\index{FP32!gradient computation} for gradient computation and weight updates, bidirectional data flow for backpropagation\index{Backpropagation!memory requirements}[^fn-backpropagation-memory], and large memory capacity for storing activations. Inference can exploit reduced precision (INT8 or INT4), requires only forward computation, and prioritizes latency over throughput[^fn-latency-throughput]. These differences drive specialized architectures: training accelerators maximize FLOPS and memory bandwidth, while inference accelerators optimize for energy efficiency and deterministic latency. -[^fn-backpropagation-memory]: **Backpropagation**: Short for "backward propagation of errors," formalized by Rumelhart, Hinton, and Williams in 1986, though the mathematical foundation (reverse-mode automatic differentiation) traces to the 1960s. The name describes its mechanism: errors flow backward from output to input, propagating gradients through the network via the chain rule. This bidirectional data flow requires storing all intermediate activations, increasing memory requirements 2–3$\times$ versus inference-only forward passes. +[^fn-backpropagation-memory]: **Backpropagation**: Short for "backward propagation of errors," formalized by Rumelhart, Hinton, and Williams in 1986, though the mathematical foundation (reverse-mode automatic differentiation) traces to the 1960s. The name describes its mechanism: errors flow backward from output to input, propagating gradients through the network via the chain rule. This bidirectional data flow requires storing all intermediate activations, increasing memory requirements 2–3 $\times$ versus inference-only forward passes. [^fn-latency-throughput]: **Latency vs Throughput**: "Latency" from Latin "latere" (to lie hidden), originally described the delay before something becomes apparent. "Throughput" emerged from industrial manufacturing, measuring production rate. In computing, latency measures single-request response time (milliseconds), while throughput measures processing rate (requests/second). Training optimizes throughput for batch processing; inference prioritizes latency for real-time responses. @@ -626,7 +626,7 @@ Hardware specialization is driven by energy physics. **The Energy Inversion** -- [ ] **Data Movement Cost**: Can you explain why moving data from DRAM costs 100$\times$ more energy than computing on it? +- [ ] **Data Movement Cost**: Can you explain why moving data from DRAM costs 100 $\times$ more energy than computing on it? - [ ] **Architectural Response**: How do **Systolic Arrays** (TPU) and **Tensor Cores** (GPU) minimize this cost? (They reuse data in registers for many operations before writing back; see @sec-hardware-acceleration-systolic-arrays-6fa8 for details.) **Selection Logic** @@ -663,15 +663,15 @@ Neural networks are characterized by three unique properties that drive this shi [^fn-scratchpad-etymology]: **Scratchpad**: From the physical scratch pads used for quick calculations before computers. In hardware, scratchpad memory is software-managed fast memory near compute units, contrasting with caches that are hardware-managed. Unlike caches that guess what data to keep, scratchpads give programmers explicit control over what data resides in fast memory, enabling the predictable data movement that ML workloads require. -3. **Tolerance for Reduced Precision**\index{Quantization!reduced precision tolerance}: Neural networks typically remain robust even when using 8-bit or 4-bit integers instead of 64-bit floating-point numbers. This flexibility allows architects to fit 10$\times$ more compute units in the same silicon area. +3. **Tolerance for Reduced Precision**\index{Quantization!reduced precision tolerance}: Neural networks typically remain robust even when using 8-bit or 4-bit integers instead of 64-bit floating-point numbers. This flexibility allows architects to fit 10 $\times$ more compute units in the same silicon area. The primary engineering challenge is no longer "how fast can we calculate?" but "how close can we keep the data to the calculation?" In modern accelerators, accessing data from external memory (DRAM)\index{DRAM!energy cost} can consume $100\times$ more energy than the actual arithmetic operation. This disparity is precisely why the accelerator architecture in @fig-accelerator-anatomy prioritizes high-bandwidth memory (HBM)\index{HBM!High Bandwidth Memory}[^fn-hbm-architecture] and large on-chip scratchpads\index{Scratchpad Memory!accelerator design} over simply adding more compute units. -[^fn-hbm-architecture]: HBM (mentioned in @sec-network-architectures) achieves 2–10$\times$ higher bandwidth than GDDR memory through 3D die stacking with thousands of TSVs. From a hardware architecture perspective, HBM's 2-3 TB/s bandwidth (vs. 500-700 GB/s for GDDR6X) transforms memory-bound ML workloads toward compute-bound performance. The trade-off is higher manufacturing cost, limiting HBM to data center accelerators where bandwidth justifies the premium. +[^fn-hbm-architecture]: HBM (mentioned in @sec-network-architectures) achieves 2–10 $\times$ higher bandwidth than GDDR memory through 3D die stacking with thousands of TSVs. From a hardware architecture perspective, HBM's 2-3 TB/s bandwidth (vs. 500-700 GB/s for GDDR6X) transforms memory-bound ML workloads toward compute-bound performance. The trade-off is higher manufacturing cost, limiting HBM to data center accelerators where bandwidth justifies the premium. To see how accelerators address this integration bottleneck in practice, examine the architectural blueprint in @fig-accelerator-anatomy. Notice how every design decision, from the processing element grid to the multi-level cache hierarchy, targets data movement reduction rather than raw compute multiplication. -::: {#fig-accelerator-anatomy fig-env="figure" fig-pos="htb" fig-cap="**Anatomy of a Modern AI Accelerator**: AI accelerators integrate specialized processing elements containing tensor cores, vector units, and special function units, supported by a hierarchical memory system from high-bandwidth memory down to local caches. This architecture maximizes data reuse and parallel execution while minimizing energy-intensive data movement, forming the foundation for 100–1,000$\times$ performance improvements over general-purpose processors." fig-alt="Block diagram showing AI accelerator architecture: CPU connects to DRAM stacks and processing element grid containing tensor cores, vector units, and local caches in hierarchical arrangement."} +::: {#fig-accelerator-anatomy fig-env="figure" fig-pos="htb" fig-cap="**Anatomy of a Modern AI Accelerator**: AI accelerators integrate specialized processing elements containing tensor cores, vector units, and special function units, supported by a hierarchical memory system from high-bandwidth memory down to local caches. This architecture maximizes data reuse and parallel execution while minimizing energy-intensive data movement, forming the foundation for 100–1,000 $\times$ performance improvements over general-purpose processors." fig-alt="Block diagram showing AI accelerator architecture: CPU connects to DRAM stacks and processing element grid containing tensor cores, vector units, and local caches in hierarchical arrangement."} ```{.tikz} \begin{tikzpicture}[line cap=round,line join=round,font=\usefont{T1}{phv}{m}{n}\small] \tikzset{ @@ -846,7 +846,7 @@ fit=(BB1)(DPE1)(DPE4)(B1),yshift=0mm](BB2){}; ``` ::: -The evolution from the Intel 8087 to the Google TPU reveals a consistent pattern: hardware evolves to fit the algorithm's dominant bottleneck. Where the 8087 addressed floating-point operations that consumed 80% of scientific computing time, modern AI accelerators address matrix operations that constitute over 95% of neural network computation. This concentration of demand explains why specialized AI silicon achieves 100–1,000$\times$ performance improvements over general-purpose processors. +The evolution from the Intel 8087 to the Google TPU reveals a consistent pattern: hardware evolves to fit the algorithm's dominant bottleneck. Where the 8087 addressed floating-point operations that consumed 80% of scientific computing time, modern AI accelerators address matrix operations that constitute over 95% of neural network computation. This concentration of demand explains why specialized AI silicon achieves 100–1,000 $\times$ performance improvements over general-purpose processors. The constraints identified above (massive parallelism, predictable data flow, and tolerance for reduced precision) shape accelerator architecture. Before examining the computational primitives that exploit these characteristics, we examine the architectural organization that enables their efficient execution. Modern AI accelerators achieve their dramatic performance improvements through a carefully orchestrated hierarchy of specialized components operating in concert. @@ -871,7 +871,7 @@ We call the hardware units that exploit these patterns *AI compute primitives*: ::: {#lst-dense_layer_def lst-cap="**Dense Layer Abstraction**: High-level framework APIs encapsulate 131,072 multiply-accumulate operations (256 inputs times 512 outputs) in a single function call, hiding the computational complexity from developers while enabling automatic hardware optimization."} ```{.python} # Framework abstracts compute-intensive operations -dense = Dense(512)(input_tensor) # 256×512 = 131K MACs per sample +dense = Dense(512)(input_tensor) # 256 $\times$ 512 = 131K MACs per sample ``` ::: @@ -879,11 +879,12 @@ This single line of code conceals the computational complexity that accelerators ::: {#lst-dense_expansion lst-cap="**Matrix Operation Expansion**: Each dense layer decomposes into matrix multiplication and element-wise operations, exposing the dominant compute pattern that consumes over 95% of neural network execution time."} ```{.python} -# Linear transformation: O(input_dim × output_dim × batch) operations +# Linear transformation: O(input_dim $\times$ output_dim $\times$ +# batch) operations output = ( matmul(input, weights) + bias ) # Matrix multiply dominates cost -output = activation(output) # Element-wise: O(output_dim × batch) +output = activation(output) # Element-wise: O(output_dim $\times$ batch) ``` ::: @@ -891,14 +892,16 @@ The matrix multiplication dominates computation time, but this abstraction still ::: {#lst-loop_level_dense lst-cap="**Processor-Level Execution**: Nested loops reveal the O(batch $\times$ input $\times$ output) multiply-accumulate operations that accelerators must execute, with 4 million MACs for typical batch=32, input=256, output=512 configurations."} ```{.python} -# Total operations: batch_size × output_size × input_size MACs +# Total operations: batch_size $\times$ output_size $\times$ +# input_size MACs for n in range(batch_size): # Batch dimension: parallelizable for m in range(output_size): # Output neurons: parallelizable sum = bias[m] # Initialize accumulator for k in range(input_size): # Reduction dimension: sequential sum += input[n, k] * weights[k, m] # MAC operation output[n, m] = activation(sum) # Non-linear transformation -# Example: 32 × 512 × 256 = 4.2M multiply-accumulate operations +# Example: 32 $\times$ 512 $\times$ 256 = 4.2M multiply-accumulate +# operations ``` ::: @@ -950,7 +953,7 @@ Key vector operations map directly to common deep learning patterns. @tbl-vector These efficiency gains extend beyond instruction count reduction. Memory bandwidth utilization improves as vector loads transfer multiple values per operation, and energy efficiency increases because control logic is amortized across many data elements. These improvements compound across the deep layers of modern neural networks, where billions of element-wise operations execute per forward pass. The architectural pattern is not new — the Cray-1\index{Cray-1!vector computing legacy}[^fn-cray-vector] pioneered the same approach for scientific computing in 1975 [@jordan1982guide] — but neural networks have given it unprecedented commercial importance. -[^fn-cray-vector]: **Cray-1 Vector Legacy**: The Cray-1 (1975) cost $8.8 million (approximately $40-45 million in 2024 dollars) but could perform 160 million floating-point operations per second, 1000$\times$ faster than typical computers. Its 64-element vector registers and pipelined vector units established the architectural template that modern AI accelerators still follow: process many data elements simultaneously with specialized hardware pipelines. +[^fn-cray-vector]: **Cray-1 Vector Legacy**: The Cray-1 (1975) cost $8.8 million (approximately $40-45 million in 2024 dollars) but could perform 160 million floating-point operations per second, 1000 $\times$ faster than typical computers. Its 64-element vector registers and pipelined vector units established the architectural template that modern AI accelerators still follow: process many data elements simultaneously with specialized hardware pipelines. Vector operations excel at element-wise transformations like activation functions, where each output depends only on its corresponding input. But neural networks also require *structured* computations where each output depends on *all* inputs — the weighted sums that define layer transformations. These many-to-many operations naturally express themselves as matrix multiplications, our second compute primitive. @@ -971,8 +974,8 @@ layer = nn.Linear(256, 512) # Layer transforms 256 inputs to output = layer(input_batch) # Process a batch of 32 samples # Framework Internal: Core operations -Z = matmul(weights, input) # Matrix: transforms [256 × 32] -# input to [512 × 32] output +Z = matmul(weights, input) # Matrix: transforms [256 $\times$ 32] +# input to [512 $\times$ 32] output Z = Z + bias # Vector: adds bias to each # output independently output = relu(Z) # Vector: applies activation to @@ -1015,7 +1018,7 @@ Neural networks employ matrix operations across diverse architectural patterns b \index{Convolution!matrix multiplication equivalence} Matrix operations appear consistently across modern neural architectures. Convolution operations transform into matrix multiplications through the im2col technique\index{im2col!convolution to matrix}[^fn-im2col-hardware], enabling efficient execution on matrix-optimized hardware. @lst-matrix_patterns illustrates these diverse applications. -[^fn-im2col-hardware]: **Im2col (Image-to-Column)**: A preprocessing technique that converts convolution operations into matrix multiplications by unfolding image patches into column vectors. A 3$\times$3 convolution on a 224$\times$224 image creates a matrix with ~50,000 columns, enabling efficient GEMM execution but increasing memory usage 9$\times$ due to overlapping patches. This transformation explains why convolutions are actually matrix operations in modern ML accelerators. +[^fn-im2col-hardware]: **Im2col (Image-to-Column)**: A preprocessing technique that converts convolution operations into matrix multiplications by unfolding image patches into column vectors. A 3 $\times$ 3 convolution on a 224 $\times$ 224 image creates a matrix with ~50,000 columns, enabling efficient GEMM execution but increasing memory usage 9 $\times$ due to overlapping patches. This transformation explains why convolutions are actually matrix operations in modern ML accelerators. ::: {#lst-matrix_patterns lst-cap="**Linear Layers**: Layer transformations combine input features to produce hidden representations. Matrix operations in neural networks enable efficient feature extraction and transformation, forming the backbone of many machine learning architectures."} ```{.python} @@ -1044,11 +1047,11 @@ This pervasive pattern of matrix multiplication has direct implications for hard #### Matrix Operations Hardware Acceleration {#sec-hardware-acceleration-matrix-operations-hardware-acceleration-514a} \index{Matrix Operations!hardware acceleration} -The computational demands of matrix operations have driven specialized hardware optimizations. @lst-matrix_unit demonstrates how modern processors implement dedicated matrix units that process entire 16$\times$16 blocks simultaneously, achieving 32$\times$ higher throughput than vector processing alone. +The computational demands of matrix operations have driven specialized hardware optimizations. @lst-matrix_unit demonstrates how modern processors implement dedicated matrix units that process entire 16 $\times$ 16 blocks simultaneously, achieving 32 $\times$ higher throughput than vector processing alone. ::: {#lst-matrix_unit lst-cap="**Matrix Unit Operation**: Enables efficient block-wise matrix multiplication and accumulation in hardware-accelerated systems, demonstrating how specialized units streamline computational tasks for AI/ML operations."} ```{.c} -mload mr1, (weight_ptr) # Load e.g., 16 × 16 block of +mload mr1, (weight_ptr) # Load e.g., 16 $\times$ 16 block of # weight matrix mload mr2, (input_ptr) # Load corresponding input block matmul.mm mr3, mr1, mr2 # Multiply and accumulate entire @@ -1238,7 +1241,7 @@ The listing above shows a CUDA[^fn-cuda] kernel where SIMT execution allows neur #### Tensor Cores {#sec-hardware-acceleration-tensor-cores-771f} \index{Tensor Cores!definition} -Consider a single transformer attention head computing the $Q \times K^T$ product for a 2048-token sequence with 64-dimensional embeddings. This operation requires multiplying a $2048 \times 64$ matrix by a $64 \times 2048$ matrix — roughly 537 million multiply-accumulate operations. On a scalar processor executing one operation per cycle at 2 GHz, this single attention head would take 268 milliseconds. A GPU's SIMT execution reduces this to roughly 34 milliseconds through thread-level parallelism. But a tensor core, processing entire $16 \times 16$ matrix tiles per instruction, completes the same operation in under 0.5 milliseconds — a 500$\times$ improvement over scalar execution. This dramatic speedup arises not from faster clock speeds but from a fundamentally different approach to organizing computation around matrix blocks rather than individual elements. +Consider a single transformer attention head computing the $Q \times K^T$ product for a 2048-token sequence with 64-dimensional embeddings. This operation requires multiplying a $2048 \times 64$ matrix by a $64 \times 2048$ matrix — roughly 537 million multiply-accumulate operations. On a scalar processor executing one operation per cycle at 2 GHz, this single attention head would take 268 milliseconds. A GPU's SIMT execution reduces this to roughly 34 milliseconds through thread-level parallelism. But a tensor core, processing entire $16 \times 16$ matrix tiles per instruction, completes the same operation in under 0.5 milliseconds — a 500 $\times$ improvement over scalar execution. This dramatic speedup arises not from faster clock speeds but from a fundamentally different approach to organizing computation around matrix blocks rather than individual elements. While SIMD and SIMT units provide efficient execution of vector operations, neural networks rely heavily on matrix computations\index{Matrix Operations!neural network workloads} that require specialized execution units for structured multi-dimensional processing. The energy economics of matrix operations drive this specialization: traditional scalar processing can require multiple off-chip memory accesses per operation, while tensor cores\index{Tensor Cores!energy efficiency} amortize data movement across entire matrix blocks. Tensor processing units extend SIMD and SIMT principles by enabling efficient matrix operations through dedicated hardware blocks (**tensor cores**) that execute matrix multiplications and accumulations on matrix tiles. In many cases, this shifts the dominant cost from off-chip data movement toward on-chip reuse and arithmetic, depending on the kernel mix and memory behavior. @@ -1264,9 +1267,9 @@ Tensor processing unit architectures differ based on design priorities. Some GPU [^fn-hwacc-neural-engine]: **On-Device Neural Engine Strategy**: Many mobile SoCs include dedicated neural engines (NPUs) to enable on-device ML within tight battery and thermal envelopes. These blocks can accelerate common inference kernels efficiently, supporting interactive features such as vision and speech processing without requiring cloud connectivity. -The increasing specialization of AI hardware has driven measurable performance improvements in deep learning workloads. To appreciate the magnitude of this shift, trace the curve in @fig-ai-performance from left to right: over a single decade, NVIDIA GPU performance jumped roughly 1,000$\times$ as the architecture transitioned from general-purpose floating-point execution units to highly optimized tensor processing cores. +The increasing specialization of AI hardware has driven measurable performance improvements in deep learning workloads. To appreciate the magnitude of this shift, trace the curve in @fig-ai-performance from left to right: over a single decade, NVIDIA GPU performance jumped roughly 1,000 $\times$ as the architecture transitioned from general-purpose floating-point execution units to highly optimized tensor processing cores. -![**GPU Performance Scaling**: NVIDIA single-chip inference performance increased by approximately 1,000$\times$ over a decade, from 3.9 TFLOPS (FP32) on the K20X to 4,000 TOPS (FP8 Sparse) on the H100. This three-orders-of-magnitude gain was driven by architectural innovations transitioning from general-purpose floating-point to dedicated tensor core acceleration, reduced precision (FP16, INT8, FP8), and hardware-accelerated structured sparsity.](images/svg/int8_tops.svg){#fig-ai-performance fig-alt="Line graph of NVIDIA GPU inference performance from 2012 to 2023 showing exponential growth from K20X at 3.9 TFLOPS to H100 at 4000 TOPS, a 1,000× increase over the decade."} +![**GPU Performance Scaling**: NVIDIA single-chip inference performance increased by approximately 1,000 $\times$ over a decade, from 3.9 TFLOPS (FP32) on the K20X to 4,000 TOPS (FP8 Sparse) on the H100. This three-orders-of-magnitude gain was driven by architectural innovations transitioning from general-purpose floating-point to dedicated tensor core acceleration, reduced precision (FP16, INT8, FP8), and hardware-accelerated structured sparsity.](images/svg/int8_tops.svg){#fig-ai-performance fig-alt="Line graph of NVIDIA GPU inference performance from 2012 to 2023 showing exponential growth from K20X at 3.9 TFLOPS to H100 at 4000 TOPS, a 1,000 $\times$ increase over the decade."} #### Processing Elements {#sec-hardware-acceleration-processing-elements-daa1} @@ -1737,7 +1740,7 @@ The "Systolic" (heartbeat) metaphor is not just about timing; it reflects a deci - **Data Movement**: 2 loads per `{python} systolic_size_str` operations = `{python} systolic_access_str` DRAM accesses (per operation). - **Energy**: ≈ `{python} systolic_access_str` $\times$ `{python} dram_access_str` pJ + `{python} compute_energy_str` pJ (compute) ≈ **`{python} systolic_energy_str` pJ/OP**. -**The Systems Conclusion**: A systolic array is **`{python} energy_ratio_str`$\times$ more energy-efficient** than a naive vector unit for large matrix multiplications. +**The Systems Conclusion**: A systolic array is **`{python} energy_ratio_str` $\times$ more energy-efficient** than a naive vector unit for large matrix multiplications. - This efficiency is what allows a Google TPU to pack 100,000+ MAC units into a single chip without melting. - **The Limitation**: This "Energy Dividend" only pays out if the matrix is large enough to fill the array. For small matrices (common in real-time inference), the array is under-utilized, and the energy efficiency drops back toward the vector unit baseline. ::: @@ -1869,7 +1872,7 @@ There is no "perfect" accelerator. A chip optimized for Weight-Stationary flow ( The synchronized data flow ensures that matrix element A[i,k] encounters corresponding B[k,j] elements at precise temporal intervals, executing the multiply-accumulate operations required for matrix multiplication C[i,j] = Σ A[i,k] $\times$ B[k,j]. This systematic reuse of operands across multiple processing elements substantially reduces memory bandwidth requirements by eliminating redundant data fetches from external memory subsystems. -Consider the multiplication of 2$\times$2 matrices A and B within a systolic array. During the first computational cycle, element A[0,0]=2 propagates horizontally while B[0,0]=1 moves vertically, converging at processing element PE(0,0) to execute the multiplication 2$\times$1=2. In the subsequent cycle, the same A[0,0]=2 advances to PE(0,1) where it encounters B[0,1]=3, computing 2$\times$3=6. Concurrently, A[0,1]=4 enters PE(0,0) to engage with the next B matrix element. This coordinated data movement enables systematic operand reuse across multiple computational operations, eliminating redundant memory accesses and exemplifying the efficiency principle underlying systolic array architectures. +Consider the multiplication of 2 $\times$ 2 matrices A and B within a systolic array. During the first computational cycle, element A[0,0]=2 propagates horizontally while B[0,0]=1 moves vertically, converging at processing element PE(0,0) to execute the multiplication 2 $\times$ 1=2. In the subsequent cycle, the same A[0,0]=2 advances to PE(0,1) where it encounters B[0,1]=3, computing 2 $\times$ 3=6. Concurrently, A[0,1]=4 enters PE(0,0) to engage with the next B matrix element. This coordinated data movement enables systematic operand reuse across multiple computational operations, eliminating redundant memory accesses and exemplifying the efficiency principle underlying systolic array architectures. Each processing element in the array performs a multiply-accumulate operation in every cycle. In the configuration shown here (matching the example above, where matrix $A$ flows horizontally and $B$ flows vertically): @@ -1882,7 +1885,7 @@ Note that actual data flow directions vary across implementations; some architec This structured computation model minimizes data movement between global memory and processing elements, improving both efficiency and scalability. As systolic arrays operate in a streaming fashion, they are particularly effective for high-throughput workloads such as deep learning training and inference. -While @fig-systolic-array captures the core dataflow principle, systolic architectures vary significantly across different accelerator designs in practice. Training-focused architectures like Google's TPU employ large arrays (128$\times$128 or larger) optimized for high computational throughput, while inference-oriented designs found in edge devices prioritize energy efficiency with smaller configurations (8$\times$8 to 32$\times$32). +While @fig-systolic-array captures the core dataflow principle, systolic architectures vary significantly across different accelerator designs in practice. Training-focused architectures like Google's TPU employ large arrays (128 $\times$ 128 or larger) optimized for high computational throughput, while inference-oriented designs found in edge devices prioritize energy efficiency with smaller configurations (8 $\times$ 8 to 32 $\times$ 32). The underlying principle remains consistent: data flows systematically through processing elements, with inputs moving horizontally and vertically to compute partial sums in a synchronized fashion. However, as detailed in @sec-hardware-acceleration-understanding-ai-memory-wall-3ea9, practical effectiveness is ultimately constrained by memory bandwidth bottlenecks. @@ -1913,13 +1916,13 @@ systolic_dim_str = fmt(systolic_dim_value, precision=0, commas=False) systolic_ops_str = f"{systolic_ops_value:,}" ``` -A `{python} systolic_dim_str`$\times$`{python} systolic_dim_str` systolic array capable of `{python} systolic_ops_str` operations per cycle requires continuous data feed to maintain utilization. Each cycle demands fresh input activations and weight parameters that must traverse from off-chip memory through on-chip buffers to the array edges. The TPU's 1,200 GB/s on-chip bandwidth enables high utilization, but even this substantial bandwidth becomes limiting when processing large transformer models where memory requirements exceed on-chip capacity. +A `{python} systolic_dim_str` $\times$ `{python} systolic_dim_str` systolic array capable of `{python} systolic_ops_str` operations per cycle requires continuous data feed to maintain utilization. Each cycle demands fresh input activations and weight parameters that must traverse from off-chip memory through on-chip buffers to the array edges. The TPU's 1,200 GB/s on-chip bandwidth enables high utilization, but even this substantial bandwidth becomes limiting when processing large transformer models where memory requirements exceed on-chip capacity. -Recall from @sec-model-compression that quantization reduces model memory footprint by converting FP32 weights to INT8 representations. This optimization directly addresses the memory bandwidth constraints identified here. Converting 32-bit floating-point weights to 8-bit integers reduces memory traffic by 4$\times$, transforming bandwidth-bound operations into compute-bound workloads where systolic arrays can achieve higher utilization. Similarly, structured pruning removes entire rows or columns of weight matrices, reducing both the data volume that must traverse memory hierarchies and the computation required. These algorithmic optimizations prove valuable precisely because they target the memory bottleneck that limits accelerator performance in practice. +Recall from @sec-model-compression that quantization reduces model memory footprint by converting FP32 weights to INT8 representations. This optimization directly addresses the memory bandwidth constraints identified here. Converting 32-bit floating-point weights to 8-bit integers reduces memory traffic by 4 $\times$, transforming bandwidth-bound operations into compute-bound workloads where systolic arrays can achieve higher utilization. Similarly, structured pruning removes entire rows or columns of weight matrices, reducing both the data volume that must traverse memory hierarchies and the computation required. These algorithmic optimizations prove valuable precisely because they target the memory bottleneck that limits accelerator performance in practice. #### Numerics in AI Acceleration {#sec-hardware-acceleration-numerics-ai-acceleration-f7be} -Systolic arrays and tensor cores achieve their efficiency partly through specialized support for reduced-precision arithmetic. This connection is direct: the 2$\times$ speedup from FP16 versus FP32 is not merely "using fewer bits" but reflects that accelerators physically pack 2$\times$ more FP16 multiply-accumulate units into the same silicon area. Building on the quantization and mixed-precision techniques established in @sec-model-compression, this section examines how AI accelerators implement hardware support for these reduced-precision formats. The efficiency of AI accelerators is not determined by computational power alone but also by how effectively the hardware supports different numerical representations. The choice of numerical format shapes the balance between accuracy, throughput, and energy consumption, influencing how different execution units, such as SIMD and SIMT units, tensor cores, and systolic arrays, are designed and deployed. +Systolic arrays and tensor cores achieve their efficiency partly through specialized support for reduced-precision arithmetic. This connection is direct: the 2 $\times$ speedup from FP16 versus FP32 is not merely "using fewer bits" but reflects that accelerators physically pack 2 $\times$ more FP16 multiply-accumulate units into the same silicon area. Building on the quantization and mixed-precision techniques established in @sec-model-compression, this section examines how AI accelerators implement hardware support for these reduced-precision formats. The efficiency of AI accelerators is not determined by computational power alone but also by how effectively the hardware supports different numerical representations. The choice of numerical format shapes the balance between accuracy, throughput, and energy consumption, influencing how different execution units, such as SIMD and SIMT units, tensor cores, and systolic arrays, are designed and deployed. ##### Precision Trade-offs {#sec-hardware-acceleration-precision-tradeoffs-8fa8} @@ -1949,7 +1952,7 @@ This shift toward precision diversity is evident in the evolution of AI hardware Newer architectures incorporate a growing diversity of numerical formats, reflecting the need for greater flexibility across different AI workloads. This trend suggests that future AI accelerators will continue expanding support for adaptive precision, balancing computational efficiency against model accuracy. -The precision format used in hardware design has cascading implications across the entire system. Reducing from FP32 to FP16 cuts memory traffic in half, which matters far more than it might seem: because memory access dominates energy consumption (recall the orders-of-magnitude DRAM-to-compute energy gap from @sec-hardware-acceleration-understanding-ai-memory-wall-3ea9), halving memory traffic can nearly halve total energy per inference. Simultaneously, tensor cores and systolic arrays can pack twice as many FP16 multiply-accumulate units into the same silicon area, doubling peak throughput. Integer formats push this further — INT8 arithmetic requires roughly 30$\times$ less energy than FP32 per operation, which is why inference-focused accelerators like the TPUv1 were built around INT8 from the start. The systems insight is that reduced precision does not merely "save bits" — it simultaneously relieves the memory bandwidth bottleneck and increases compute density, attacking both sides of the roofline simultaneously. +The precision format used in hardware design has cascading implications across the entire system. Reducing from FP32 to FP16 cuts memory traffic in half, which matters far more than it might seem: because memory access dominates energy consumption (recall the orders-of-magnitude DRAM-to-compute energy gap from @sec-hardware-acceleration-understanding-ai-memory-wall-3ea9), halving memory traffic can nearly halve total energy per inference. Simultaneously, tensor cores and systolic arrays can pack twice as many FP16 multiply-accumulate units into the same silicon area, doubling peak throughput. Integer formats push this further — INT8 arithmetic requires roughly 30 $\times$ less energy than FP32 per operation, which is why inference-focused accelerators like the TPUv1 were built around INT8 from the start. The systems insight is that reduced precision does not merely "save bits" — it simultaneously relieves the memory bandwidth bottleneck and increases compute density, attacking both sides of the roofline simultaneously. As AI models continue to scale in size, accelerator architectures are evolving to support more efficient numerical formats. Future designs are expected to incorporate adaptive precision techniques, dynamically adjusting computation precision based on workload characteristics. Understanding how these execution units and precision formats integrate into complete accelerator architectures reveals the full picture of AI hardware design. @@ -2092,11 +2095,11 @@ The table reveals several important patterns. First, price-performance improves Framework selection significantly impacts these economic decisions. Detailed hardware-framework optimization strategies are covered in @sec-ml-frameworks, while performance evaluation methodologies are discussed in @sec-benchmarking. -The preceding sections revealed impressive computational machinery: vector units achieving 8$\times$ parallelism through SIMD execution, matrix operations processing 256 elements simultaneously, and tensor cores executing 16$\times$16$\times$16 fused multiply-accumulate blocks in single cycles. An NVIDIA A100's tensor cores can execute `{python} a100_tflops_fp16` trillion operations per second, and an H100 pushes this further to nearly 2 petaFLOPS (dense) in FP8 precision. At these rates, the pure arithmetic for a ResNet-50 forward pass could complete in microseconds. +The preceding sections revealed impressive computational machinery: vector units achieving 8 $\times$ parallelism through SIMD execution, matrix operations processing 256 elements simultaneously, and tensor cores executing 16 $\times$ 16 $\times$ 16 fused multiply-accumulate blocks in single cycles. An NVIDIA A100's tensor cores can execute `{python} a100_tflops_fp16` trillion operations per second, and an H100 pushes this further to nearly 2 petaFLOPS (dense) in FP8 precision. At these rates, the pure arithmetic for a ResNet-50 forward pass could complete in microseconds. Looking ahead, the Blackwell (B200) architecture extends this trend by introducing native FP4 support, with NVIDIA reporting up to 4.5 petaFLOPS (dense) or 9 petaFLOPS (sparse) peak throughput in FP4 per chip. This confirms the precision bottleneck trend: as models grow, hardware adapts by trading precision for massive parallelism, requiring systems engineers to master progressively lower-bit numerics (FP8, FP4) to unlock the silicon's full potential. -Yet real ResNet-50 inference takes milliseconds, not microseconds. The gap between theoretical capability and practical performance reveals the chapter's central tension, first posed in the Purpose section: computational capability has outpaced our ability to feed data to processors. Moving data from memory costs 100–1,000$\times$ more energy than arithmetic, and memory bandwidth grows at roughly 20% annually while compute throughput doubles every two years. This disparity determines whether those `{python} a100_tflops_fp16` TFLOPS translate to 30 TFLOPS of sustained performance (10% utilization) or 250 TFLOPS (80% utilization). +Yet real ResNet-50 inference takes milliseconds, not microseconds. The gap between theoretical capability and practical performance reveals the chapter's central tension, first posed in the Purpose section: computational capability has outpaced our ability to feed data to processors. Moving data from memory costs 100–1,000 $\times$ more energy than arithmetic, and memory bandwidth grows at roughly 20% annually while compute throughput doubles every two years. This disparity determines whether those `{python} a100_tflops_fp16` TFLOPS translate to 30 TFLOPS of sustained performance (10% utilization) or 250 TFLOPS (80% utilization). Understanding *why* this gap exists — and what architectural innovations address it — requires examining the memory systems that feed data to the compute primitives we have just analyzed. The memory hierarchy is not merely a supporting subsystem; it is the primary determinant of whether accelerators achieve their theoretical potential. @@ -2257,7 +2260,7 @@ ax.set_xticks(years) plt.show() ``` -Beyond performance limitations, memory access imposes a steep energy cost. Fetching data from off-chip DRAM consumes far more energy than performing arithmetic operations [@horowitz2014computing]. This inefficiency is particularly evident in machine learning models, where large parameter sizes, frequent memory accesses, and non-uniform data movement patterns exacerbate memory bottlenecks. The energy differential drives architectural decisions: Google's TPU achieves 30–83$\times$ better energy efficiency than contemporary GPUs by minimizing data movement through systolic arrays and large on-chip memory. These design choices demonstrate that energy constraints, not computational limits, often determine practical deployment feasibility. +Beyond performance limitations, memory access imposes a steep energy cost. Fetching data from off-chip DRAM consumes far more energy than performing arithmetic operations [@horowitz2014computing]. This inefficiency is particularly evident in machine learning models, where large parameter sizes, frequent memory accesses, and non-uniform data movement patterns exacerbate memory bottlenecks. The energy differential drives architectural decisions: Google's TPU achieves 30–83 $\times$ better energy efficiency than contemporary GPUs by minimizing data movement through systolic arrays and large on-chip memory. These design choices demonstrate that energy constraints, not computational limits, often determine practical deployment feasibility. #### Memory Access Patterns in ML Workloads {#sec-hardware-acceleration-memory-access-patterns-ml-workloads-a960} @@ -2355,7 +2358,7 @@ T_{\text{compute}} = \frac{\text{FLOPs}}{R_{\text{peak}}}, $$ where the number of floating-point operations (FLOPs) is divided by the peak hardware throughput $R_{\text{peak}}$ (FLOPs/second), yielding $T_{\text{compute}}$ in seconds. When $T_{\text{mem}} > T_{\text{compute}}$, the system becomes memory-bound, meaning that the processing elements spend more time waiting for data than performing computations. This imbalance demonstrates the need for memory-optimized architectures and efficient data movement strategies to sustain high performance. -@fig-memory-wall quantifies this disparity for specific models and hardware generations, showing how model parameter counts have outpaced memory bandwidth improvements. The gap between these curves, from AlexNet's 60 million parameters to trillion-parameter frontier models, represents the engineering challenge that drives accelerator memory system design. Even the latest accelerators like NVIDIA's B200 (8 TB/s) and AMD's MI325X (6 TB/s) cannot close this gap: bandwidth has improved by roughly 16$\times$ since 2014, while model sizes have grown by over 10,000$\times$. +@fig-memory-wall quantifies this disparity for specific models and hardware generations, showing how model parameter counts have outpaced memory bandwidth improvements. The gap between these curves, from AlexNet's 60 million parameters to trillion-parameter frontier models, represents the engineering challenge that drives accelerator memory system design. Even the latest accelerators like NVIDIA's B200 (8 TB/s) and AMD's MI325X (6 TB/s) cannot close this gap: bandwidth has improved by roughly 16 $\times$ since 2014, while model sizes have grown by over 10,000 $\times$. #### Irregular Memory Access {#sec-hardware-acceleration-irregular-memory-access-c6ec} @@ -2522,7 +2525,7 @@ Memory bandwidth scaling follows different trajectories across accelerator desig HBM provides far higher bandwidth than commodity DDR memory, but at substantially higher cost and packaging complexity. High-bandwidth accelerators therefore trade higher memory-system cost for higher sustained performance on bandwidth-bound workloads. Edge accelerators often sacrifice bandwidth to meet tight cost and power targets while maintaining sufficient performance for inference workloads. -These bandwidth characteristics directly influence deployment decisions: cloud training prioritizes raw bandwidth for maximum model capacity, edge inference optimizes bandwidth efficiency for energy constraints, and mobile deployment balances bandwidth with cost limitations. Beyond the accelerator's internal memory system, however, data must also flow between the host CPU and the accelerator, introducing another potential bottleneck. This host-accelerator interface often becomes the unexpected chokepoint: even with 2 TB/s of HBM bandwidth on the accelerator, data must first traverse a PCIe link that provides only 64 GB/s, a 30$\times$ bandwidth reduction that can dominate total latency for small, frequent transfers. +These bandwidth characteristics directly influence deployment decisions: cloud training prioritizes raw bandwidth for maximum model capacity, edge inference optimizes bandwidth efficiency for energy constraints, and mobile deployment balances bandwidth with cost limitations. Beyond the accelerator's internal memory system, however, data must also flow between the host CPU and the accelerator, introducing another potential bottleneck. This host-accelerator interface often becomes the unexpected chokepoint: even with 2 TB/s of HBM bandwidth on the accelerator, data must first traverse a PCIe link that provides only 64 GB/s, a 30 $\times$ bandwidth reduction that can dominate total latency for small, frequent transfers. ### Host-Accelerator Communication {#sec-hardware-acceleration-hostaccelerator-communication-bb7a} @@ -2686,7 +2689,7 @@ $$ \end{aligned} $$ -System efficiency depends on keeping data as high up this hierarchy as possible. Once data drops to PCIe or Network speeds, it encounters a 30–100$\times$ slowdown. +System efficiency depends on keeping data as high up this hierarchy as possible. Once data drops to PCIe or Network speeds, it encounters a 30–100 $\times$ slowdown. This structured sequence begins with step (1), where data is copied from CPU memory to accelerator memory, as GPUs cannot directly access host memory at high speeds. A direct memory access (DMA)\index{DMA!Direct Memory Access}[^fn-dma] engine typically handles this transfer without consuming CPU cycles. In step (2), the CPU issues execution commands via APIs like CUDA, ROCm, or OpenCL. Step (3) involves parallel execution on the accelerator, where stalls can occur if data is not available when needed. Finally, in step (4), computed results are copied back to CPU memory for further processing. @@ -2757,7 +2760,7 @@ Each model type presents unique challenges that directly impact accelerator desi To address these challenges, modern AI accelerators incorporate multi-tier memory hierarchies that balance speed, capacity, and energy efficiency. On-chip SRAM caches and scratchpad memories store frequently accessed data, while high-bandwidth external memory provides scalability for large models. Efficient interconnects, such as NVLink, help alleviate host-accelerator transfer bottlenecks, particularly in transformer workloads where memory movement constraints can dominate execution time. -As ML workloads continue to grow in complexity, memory efficiency becomes as critical as raw compute power. The analysis reveals how memory systems dominate accelerator performance: DRAM access has 100$\times$ or higher energy cost than on-chip arithmetic, carefully structured memory hierarchies can improve effective bandwidth substantially, and different neural network architectures create distinct memory pressure patterns. These constraints — bandwidth limitations, energy costs, and communication overheads — determine whether theoretical computational capabilities translate into real-world performance. But how do we know if a *specific* workload is limited by compute or memory on a *given* accelerator? The memory wall analysis establishes *why* memory matters, but practitioners need a quantitative framework to predict *which* operations will bottleneck on a specific hardware configuration. Without such a framework, optimization becomes guesswork: engineers might spend weeks optimizing compute throughput for an operation that was memory-bound all along. +As ML workloads continue to grow in complexity, memory efficiency becomes as critical as raw compute power. The analysis reveals how memory systems dominate accelerator performance: DRAM access has 100 $\times$ or higher energy cost than on-chip arithmetic, carefully structured memory hierarchies can improve effective bandwidth substantially, and different neural network architectures create distinct memory pressure patterns. These constraints — bandwidth limitations, energy costs, and communication overheads — determine whether theoretical computational capabilities translate into real-world performance. But how do we know if a *specific* workload is limited by compute or memory on a *given* accelerator? The memory wall analysis establishes *why* memory matters, but practitioners need a quantitative framework to predict *which* operations will bottleneck on a specific hardware configuration. Without such a framework, optimization becomes guesswork: engineers might spend weeks optimizing compute throughput for an operation that was memory-bound all along. ## Roofline Model {#sec-hardware-acceleration-roofline-model-42ff} @@ -2895,9 +2898,9 @@ relu_below_roofline_str = RooflineGap.relu_below_roofline_str * **A100 (2020)**: `{python} a100_tflops_fp16` TF / `{python} a100_bw_tbs` TB/s ≈ **`{python} a100_ridge` FLOP/byte**. * **H100 (2023)**: `{python} h100_tflops_fp16` TF / `{python} h100_bw_tbs` TB/s ≈ **`{python} h100_ridge` FLOP/byte**. -**The Systems Conclusion**: The "bar" for compute intensity has doubled. An algorithm with AI = `{python} legacy_ai_str` FLOP/byte was **compute-bound** (good) on A100 but is **bandwidth-bound** (bad) on H100. This explains why "legacy" code often sees only `{python} bandwidth_ratio_str`$\times$ speedup on H100 (bandwidth ratio) instead of the advertised `{python} flops_ratio_str`$\times$ (FLOPs ratio). +**The Systems Conclusion**: The "bar" for compute intensity has doubled. An algorithm with AI = `{python} legacy_ai_str` FLOP/byte was **compute-bound** (good) on A100 but is **bandwidth-bound** (bad) on H100. This explains why "legacy" code often sees only `{python} bandwidth_ratio_str` $\times$ speedup on H100 (bandwidth ratio) instead of the advertised `{python} flops_ratio_str` $\times$ (FLOPs ratio). -**Practical Examples**: A standard **ReLU** performs 1 operation for every 8 bytes (0.125 FLOP/byte), placing it `{python} relu_below_roofline_str`$\times$ below the H100 roofline. A large **Dense MatMul** (batch=128) might reach 300 FLOP/byte, making it compute-bound. Most operations fall short of the ridge point, which is why **Kernel Fusion** is the most important optimization, as explored in @sec-hardware-acceleration-kernel-fusion-7faf. +**Practical Examples**: A standard **ReLU** performs 1 operation for every 8 bytes (0.125 FLOP/byte), placing it `{python} relu_below_roofline_str` $\times$ below the H100 roofline. A large **Dense MatMul** (batch=128) might reach 300 FLOP/byte, making it compute-bound. Most operations fall short of the ridge point, which is why **Kernel Fusion** is the most important optimization, as explored in @sec-hardware-acceleration-kernel-fusion-7faf. ::: \index{Depthwise Convolution!low arithmetic intensity}\index{Embedding Lookup!memory-bound operation}\index{LayerNorm!memory-bound operation}\index{Softmax!memory-bound operation} @@ -3076,7 +3079,7 @@ conv_ai_str = fmt(conv_ai, precision=0, commas=False) ::: {.callout-notebook #notebook-conv-analysis title="Convolutional Layer Analysis"} -Consider a Conv2D layer with input shape (batch=32, channels=128, height=56, width=56), output channels=256, kernel size 3$\times$3 on an A100 GPU: +Consider a Conv2D layer with input shape (batch=32, channels=128, height=56, width=56), output channels=256, kernel size 3 $\times$ 3 on an A100 GPU: *Computational Requirements*: @@ -3097,7 +3100,7 @@ AI = `{python} conv_total_gflops_str` GFLOPs / `{python} conv_total_mb_str` MB = This is **well above** A100's ridge point of `{python} a100_ridge` FLOP/byte, making this operation **compute-bound**. The layer will achieve near-peak performance of ~`{python} a100_tflops_fp16` TFLOPS (FP16 with Tensor Cores). ::: -The convolutional layer's high arithmetic intensity arises from its weight reuse pattern: the same 3$\times$3 kernel is applied across all spatial locations, amortizing the cost of loading weights across millions of output computations. This is the architectural pattern that makes CNNs so efficient on modern accelerators. +The convolutional layer's high arithmetic intensity arises from its weight reuse pattern: the same 3 $\times$ 3 kernel is applied across all spatial locations, amortizing the cost of loading weights across millions of output computations. This is the architectural pattern that makes CNNs so efficient on modern accelerators. However, not all layers in a neural network exhibit this favorable profile. The fully connected (dense) layers that typically appear at the end of classification networks, or as the projection layers in transformers, have different arithmetic intensity characteristics. A *dense layer analysis* reveals this contrast, which is essential for predicting where bottlenecks will occur in end-to-end model execution. @@ -3268,7 +3271,7 @@ LayerNorm with input shape (batch=32, seq=512, hidden=768): **Arithmetic Intensity**: AI = `{python} ln_total_mflops_str` MFLOPs / `{python} ln_total_mb_str` MB = `{python} ln_ai_str` FLOP/byte -This is **severely memory-bound** (102$\times$ below ridge point). Performance is limited to: +This is **severely memory-bound** (102 $\times$ below ridge point). Performance is limited to: Pattainable = `{python} a100_bw_str` GB/s $\times$ `{python} ln_ai_str` FLOP/byte = `{python} ln_attainable_str` TFLOPS This represents less than 1% of A100's compute capacity, explaining why normalization layers contribute negligible compute time but significant latency. @@ -3291,12 +3294,12 @@ The roofline analysis directly informs optimization priorities: 3. **Low AI (<20 FLOP/byte)**: Memory-bound operations like small dense layers, element-wise operations - Priority: Reduce memory traffic - Techniques: Aggressive operator fusion, reduce precision (FP16 → INT8), algorithmic changes - - Impact: 2-4$\times$ speedup possible through fusion alone + - Impact: 2-4 $\times$ speedup possible through fusion alone 4. **Very Low AI (<2 FLOP/byte)**: Severely memory-bound operations like normalization, activation functions - Priority: Eliminate memory round-trips - Techniques: Mandatory fusion with adjacent operations, in-place computation where possible - - Impact: Can achieve 10$\times$ speedup through fusion (e.g., LayerNorm + GELU → single fused kernel)\index{Operator Fusion!memory-bound optimization}\index{LayerNorm!kernel fusion target} + - Impact: Can achieve 10 $\times$ speedup through fusion (e.g., LayerNorm + GELU → single fused kernel)\index{Operator Fusion!memory-bound optimization}\index{LayerNorm!kernel fusion target} One of the most accessible levers for shifting an operation's position on the roofline is increasing *batch size and arithmetic intensity*. @@ -3311,7 +3314,7 @@ Example: Dense layer with M=N=2048 (FP16) - Batch=32: AI = 32 FLOP/byte (memory-bound) - Batch=256: AI = 205 FLOP/byte (compute-bound on A100) -This explains the 10–100$\times$ throughput improvement from batching in production inference systems, as MLPerf\index{MLPerf!inference benchmarking} inference scenarios demonstrate. +This explains the 10–100 $\times$ throughput improvement from batching in production inference systems, as MLPerf\index{MLPerf!inference benchmarking} inference scenarios demonstrate. ::: The batch size analysis reveals why inference serving systems are designed around batching: it changes the arithmetic intensity regime of memory-bound workloads. However, batching introduces latency trade-offs, since requests must wait in a queue until a batch forms. This tension between throughput (favoring large batches) and latency (favoring small batches) is a central challenge in ML serving systems, explored in depth in @sec-model-serving. @@ -3411,7 +3414,7 @@ As this derivation demonstrates, the Roofline model provides the diagnostic fram \index{Hardware Mapping!computational graph to hardware} The Roofline analysis taught us to diagnose whether specific operations are compute-bound or memory-bound on given hardware. We saw that ResNet-50's convolutions achieve high arithmetic intensity (50–200 FLOP/byte) and operate in the compute-bound regime, while GPT-2's attention layers achieve only 2–5 FLOP/byte and are severely memory-bound. But diagnosis is only half the challenge. Once we know that LayerNorm achieves just 1–2 FLOP/byte on an A100, the question becomes: *how* do we execute it efficiently despite this limitation? This is the domain of hardware mapping, the art of translating abstract computational graphs into concrete execution plans that exploit accelerator architectures while respecting their constraints. -The memory system challenges examined in @sec-hardware-acceleration-understanding-ai-memory-wall-3ea9 established *why* memory access dominates modern AI systems: DRAM access consumes 100–200$\times$ more energy than a multiply-accumulate operation [@horowitz2014computing]. The Roofline model established *how to measure* whether a workload is compute-bound or memory-bound. This section addresses the critical follow-up: *how to map* computations to maximize data reuse and minimize the energy-intensive transfers that the Roofline analysis revealed as the primary bottleneck. +The memory system challenges examined in @sec-hardware-acceleration-understanding-ai-memory-wall-3ea9 established *why* memory access dominates modern AI systems: DRAM access consumes 100–200 $\times$ more energy than a multiply-accumulate operation [@horowitz2014computing]. The Roofline model established *how to measure* whether a workload is compute-bound or memory-bound. This section addresses the critical follow-up: *how to map* computations to maximize data reuse and minimize the energy-intensive transfers that the Roofline analysis revealed as the primary bottleneck. Efficient execution of machine learning models on specialized AI acceleration hardware requires a structured approach to computation, ensuring that available resources are fully utilized while minimizing performance bottlenecks. These mapping considerations become particularly critical in distributed training scenarios, as explored in @sec-model-training. Unlike general-purpose processors, which rely on dynamic task scheduling, AI accelerators operate under a structured execution model that maximizes throughput by carefully assigning computations to processing elements. This process, known as mapping, dictates how computations are distributed across hardware resources, influencing execution speed, memory access patterns, and overall efficiency. @@ -3585,8 +3588,8 @@ The mapping strategies from the preceding section establish *where* computations Three questions structure all dataflow decisions: 1. **Which data stays local?** Weight-stationary, output-stationary, and input-stationary strategies each make different choices about what to cache near compute units, trading off different memory access patterns. -2. **How is data organized?** Tensor layouts (NHWC vs. NCHW) determine whether memory accesses align with hardware preferences, with performance impacts of 2–5$\times$. -3. **How are operations combined?** Kernel fusion and tiling restructure computation to minimize memory traffic, often achieving 2–10$\times$ speedups through reduced data movement alone. +2. **How is data organized?** Tensor layouts (NHWC vs. NCHW) determine whether memory accesses align with hardware preferences, with performance impacts of 2–5 $\times$. +3. **How are operations combined?** Kernel fusion and tiling restructure computation to minimize memory traffic, often achieving 2–10 $\times$ speedups through reduced data movement alone. By mastering these patterns, we can reason about 90% of dataflow optimization decisions without exhaustive search. We examine each question in turn, then see how they combine for specific neural network architectures including ResNet-50, GPT-2, and MLPs. @@ -3608,9 +3611,9 @@ Even when computational units are mapped efficiently, poor data movement strateg ::: {#lst-matmul_data_movement lst-cap="**Matrix Multiplication**: Data movement bottlenecks can lead to underutilized hardware resources, illustrating the importance of efficient data flow in optimizing machine learning model performance."} ```{.python} ## Matrix multiplication where: -## weights: [512 × 256] - model parameters -## input: [256 × 32] - batch of activations -## Z: [512 × 32] - output activations +## weights: [512 $\times$ 256] - model parameters +## input: [256 $\times$ 32] - batch of activations +## Z: [512 $\times$ 32] - output activations ## Computing each output element Z[i,j]: for i in range(512): @@ -3628,7 +3631,7 @@ The third challenge involves the accumulation of intermediate results. Since eac One way to mitigate these challenges is to use SIMD and SIMT execution models, which allow multiple values to be fetched in parallel. However, even with these optimizations, data movement remains a bottleneck. The issue is not just how quickly data is retrieved but how often it must be moved and where it is placed within the memory hierarchy [@han2016eie]. -Given that data movement is 100–1,000$\times$ more expensive than computation, the single most important goal of an accelerator is to minimize memory access. Dataflow strategies\index{Dataflow!optimization strategies} achieve this by maximizing data reuse\index{Data Reuse!accelerator optimization}. The question is: which data is most valuable to keep local? To address this, accelerators implement dataflow strategies that determine which data remains fixed in memory and which data is streamed dynamically. These strategies represent different answers to the central question of data locality: weight-stationary keeps model parameters local, input-stationary maintains activation data, and output-stationary preserves intermediate results. Each approach trades off different memory access patterns to maximize data reuse and minimize the energy-intensive transfers that constitute the primary bottleneck in AI acceleration. +Given that data movement is 100–1,000 $\times$ more expensive than computation, the single most important goal of an accelerator is to minimize memory access. Dataflow strategies\index{Dataflow!optimization strategies} achieve this by maximizing data reuse\index{Data Reuse!accelerator optimization}. The question is: which data is most valuable to keep local? To address this, accelerators implement dataflow strategies that determine which data remains fixed in memory and which data is streamed dynamically. These strategies represent different answers to the central question of data locality: weight-stationary keeps model parameters local, input-stationary maintains activation data, and output-stationary preserves intermediate results. Each approach trades off different memory access patterns to maximize data reuse and minimize the energy-intensive transfers that constitute the primary bottleneck in AI acceleration. ##### Weight Stationary {#sec-hardware-acceleration-weight-stationary-156a} @@ -3760,13 +3763,13 @@ The advantage of channel-major layout becomes clear when performing convolutions \index{NCHW!GPU-optimized layout} Because GPUs and TPUs rely on memory coalescing\index{Memory Coalescing!GPU optimization}[^fn-memory-coalescing], a technique in which consecutive threads fetch contiguous memory addresses, channel-major layout aligns naturally with the way these processors execute parallel computations. For example, in NVIDIA GPUs, each thread in a warp (a group of threads executed simultaneously) processes different elements of the same channel, ensuring that memory accesses are efficient and reducing the likelihood of strided memory accesses, which can degrade performance. -[^fn-memory-coalescing]: **Memory Coalescing**: From Latin "coalescere" (to grow together), describing how separate things merge into one. In GPU architecture, coalescing combines multiple memory requests from threads in a warp into a single efficient transaction when threads access consecutive addresses. Uncoalesced access (scattered addresses) reduces bandwidth by 10–20$\times$, making tensor layouts and data organization critical for GPU performance. +[^fn-memory-coalescing]: **Memory Coalescing**: From Latin "coalescere" (to grow together), describing how separate things merge into one. In GPU architecture, coalescing combines multiple memory requests from threads in a warp into a single efficient transaction when threads access consecutive addresses. Uncoalesced access (scattered addresses) reduces bandwidth by 10–20 $\times$, making tensor layouts and data organization critical for GPU performance. Despite its advantages in machine learning accelerators, channel-major layout can introduce inefficiencies when running on general-purpose CPUs. Since CPUs optimize for sequential memory access, storing all values for a single channel before moving to the next disrupts cache locality for row-wise operations. This is why many machine learning frameworks (e.g., TensorFlow, PyTorch) default to row-major (NHWC) on CPUs and channel-major (NCHW) on GPUs, optimizing for the strengths of each hardware type. Modern AI frameworks and compilers often transform tensor layouts dynamically depending on the execution environment. For instance, TensorFlow and PyTorch automatically switch between NHWC[^fn-nhwc-nchw] and NCHW based on whether a model is running on a CPU, GPU, or TPU, ensuring that the memory layout aligns with the most efficient execution path. -[^fn-nhwc-nchw]: **NHWC vs NCHW**: Tensor layout formats where letters indicate dimension order: N(batch), H(height), W(width), C(channels). NHWC stores data row-by-row with channels interleaved (CPU-friendly), while NCHW groups all values for each channel together (GPU-friendly). A 224$\times$224 RGB image in NHWC stores as [R1,G1,B1,R2,G2,B2,...] while NCHW stores as [R1,R2,...,G1,G2,...,B1,B2,...]. This seemingly minor difference can impact performance by 2–5$\times$ depending on hardware. +[^fn-nhwc-nchw]: **NHWC vs NCHW**: Tensor layout formats where letters indicate dimension order: N(batch), H(height), W(width), C(channels). NHWC stores data row-by-row with channels interleaved (CPU-friendly), while NCHW groups all values for each channel together (GPU-friendly). A 224 $\times$ 224 RGB image in NHWC stores as [R1,G1,B1,R2,G2,B2,...] while NCHW stores as [R1,R2,...,G1,G2,...,B1,B2,...]. This seemingly minor difference can impact performance by 2–5 $\times$ depending on hardware. ##### Comparing Row-Major and Channel-Major Layouts {#sec-hardware-acceleration-comparing-rowmajor-channelmajor-layouts-e410} @@ -3860,7 +3863,7 @@ Each operation produces an intermediate tensor that must be written to memory an | **Y** | `{python} tensor_mb_str` MB | | **Total Memory** | **`{python} total_mb_str` MB** | -: **Intermediate Tensor Storage.** Naive execution models require substantial memory to store intermediate tensors generated by each operation. For a 1024$\times$1024 tensor, storing intermediate results (even when only the final output is needed) quadruples the total memory footprint from 4 MB to 16 MB. Minimizing intermediate data storage is essential for improving memory efficiency. {#tbl-memory-footprint} +: **Intermediate Tensor Storage.** Naive execution models require substantial memory to store intermediate tensors generated by each operation. For a 1024 $\times$ 1024 tensor, storing intermediate results (even when only the final output is needed) quadruples the total memory footprint from 4 MB to 16 MB. Minimizing intermediate data storage is essential for improving memory efficiency. {#tbl-memory-footprint} ```{python} #| label: memory-footprint-table-calc @@ -3974,7 +3977,7 @@ At this point, you should be able to answer the first two questions from the roa **Which data stays local?** The weight-stationary, output-stationary, and input-stationary patterns each make a principled choice about which data to cache near compute units. Weight-stationary (used in Google's TPU) maximizes weight reuse for CNN workloads. Output-stationary (used in NVIDIA's tensor cores) reduces partial sum memory traffic for fully connected layers. Input-stationary minimizes input reloads for models with shared inputs across multiple filters. -**How are operations combined?** Kernel fusion eliminates intermediate memory writes by merging consecutive operations (Conv2D + BatchNorm + ReLU becomes a single kernel). This optimization is most effective for element-wise operations that share data dependencies and can achieve 2–10$\times$ speedups by avoiding round-trips to DRAM. +**How are operations combined?** Kernel fusion eliminates intermediate memory writes by merging consecutive operations (Conv2D + BatchNorm + ReLU becomes a single kernel). This optimization is most effective for element-wise operations that share data dependencies and can achieve 2–10 $\times$ speedups by avoiding round-trips to DRAM. The remaining question, *how is data organized*, brings us to tiling: the technique of partitioning computations into memory-friendly blocks. Tiling complements the stationary strategies by ensuring that whichever data we choose to keep local actually fits in fast memory. @@ -3991,7 +3994,7 @@ Tiling[^fn-tiling-etymology] is a technique used to mitigate this issue by restr Matrix multiplication, widely used in AI models, demonstrates inefficient memory access when implemented naively. @lst-naive_matmul shows how, without tiling, repeated memory accesses for the same data lead to unnecessary bandwidth consumption. -::: {#lst-naive_matmul lst-cap="**Naïve Matrix Multiplication**: Direct implementation without tiling requires O(N^3) memory accesses for N$\times$N matrices, repeatedly fetching the same elements from slow DRAM memory and limiting performance to a fraction of theoretical peak throughput."} +::: {#lst-naive_matmul lst-cap="**Naïve Matrix Multiplication**: Direct implementation without tiling requires O(N^3) memory accesses for N $\times$ N matrices, repeatedly fetching the same elements from slow DRAM memory and limiting performance to a fraction of theoretical peak throughput."} ```{.python} for i in range(N): for j in range(N): @@ -4076,7 +4079,7 @@ Tiling is based on a simple but powerful principle: instead of operating on an e Consider matrix multiplication, a key operation in machine learning workloads. The operation computes $C = A \times B$ where each element $C[i,j] = \sum_{k} A[i,k] \times B[k,j]$. The naive implementation shown earlier in @lst-naive_matmul demonstrates the core problem: every iteration of the innermost loop fetches elements from matrices $A$ and $B$ from memory, performs a multiplication, and updates matrix $C$. Because matrices are large, the processor repeatedly reloads the same values from memory, even though they were just used in previous computations. -This data movement overhead is expensive: fetching from DRAM is 100–1,000$\times$ slower than accessing on-chip cache or registers. The solution is tiling. +This data movement overhead is expensive: fetching from DRAM is 100–1,000 $\times$ slower than accessing on-chip cache or registers. The solution is tiling. ##### Performance Benefits of Tiling {#sec-hardware-acceleration-performance-benefits-tiling-e7bd} @@ -4269,7 +4272,7 @@ compiler_speedup = naive_inference_ms / optimized_inference_ms compiler_speedup_str = fmt(compiler_speedup, precision=1, commas=False) # e.g. "5.9" speedup ``` -The result: inference time drops from approximately `{python} naive_inference_ms` ms (naive execution) to approximately `{python} optimized_inference_ms` ms (optimized), roughly a `{python} compiler_speedup_str`$\times$ improvement from compilation alone, before any algorithmic changes to the model. This concrete example illustrates how the dataflow strategies from the previous section, including kernel fusion (@sec-hardware-acceleration-kernel-fusion-7faf) and tiling (@sec-hardware-acceleration-memoryefficient-tiling-strategies-9fce), translate into real performance through systematic compiler optimization. +The result: inference time drops from approximately `{python} naive_inference_ms` ms (naive execution) to approximately `{python} optimized_inference_ms` ms (optimized), roughly a `{python} compiler_speedup_str` $\times$ improvement from compilation alone, before any algorithmic changes to the model. This concrete example illustrates how the dataflow strategies from the previous section, including kernel fusion (@sec-hardware-acceleration-kernel-fusion-7faf) and tiling (@sec-hardware-acceleration-memoryefficient-tiling-strategies-9fce), translate into real performance through systematic compiler optimization. This process exemplifies the hardware-software co-design principle established in @sec-hardware-acceleration-ai-hardware-acceleration-fundamentals-9b28, where machine learning compilers bridge high-level model representations with low-level hardware execution. The compiler optimizes models by restructuring computations, selecting efficient execution kernels, and maximizing hardware utilization [@chen2018tvm]. Unlike traditional compilers designed for general-purpose computing, ML workloads require specialized approaches for tensor computations and parallel execution. @@ -4302,7 +4305,7 @@ Most practitioners never interact directly with ML compilers, yet compiler quali - **Select kernels** from libraries containing hundreds of hand-tuned implementations - **Transform tensor layouts** between what your code expects and what hardware prefers -This matters practically: the same model definition can run 2–5$\times$ faster simply by switching compilation backends (e.g., PyTorch eager mode vs. torch.compile with different backends). When performance does not meet expectations, compiler configuration and backend selection are often the first optimization levers, requiring no changes to model architecture or training procedure. +This matters practically: the same model definition can run 2–5 $\times$ faster simply by switching compilation backends (e.g., PyTorch eager mode vs. torch.compile with different backends). When performance does not meet expectations, compiler configuration and backend selection are often the first optimization levers, requiring no changes to model architecture or training procedure. ::: @@ -4412,7 +4415,7 @@ Unlike the previous phases, which required AI-specific optimizations, code gener For CPUs and GPUs, AI compilers typically generate machine code or optimized assembly instructions, while for TPUs, FPGAs\index{FPGA!Field-Programmable Gate Array}[^fn-fpga], and other accelerators, the output may be optimized bytecode or execution graphs that are interpreted by the hardware's runtime system. -[^fn-fpga]: **FPGA (Field-Programmable Gate Array)**: "Field-programmable" means configurable after leaving the factory, in the customer's "field" of deployment, contrasting with factory-programmed ASICs. Xilinx (now AMD) introduced the first commercial FPGA in 1985. The "gate array" refers to the matrix of logic blocks that can be wired together through programmable interconnects. FPGAs achieve 2–10$\times$ better performance per watt than GPUs for specific workloads but require hardware description languages (Verilog/VHDL), limiting adoption compared to GPU programming. +[^fn-fpga]: **FPGA (Field-Programmable Gate Array)**: "Field-programmable" means configurable after leaving the factory, in the customer's "field" of deployment, contrasting with factory-programmed ASICs. Xilinx (now AMD) introduced the first commercial FPGA in 1985. The "gate array" refers to the matrix of logic blocks that can be wired together through programmable interconnects. FPGAs achieve 2–10 $\times$ better performance per watt than GPUs for specific workloads but require hardware description languages (Verilog/VHDL), limiting adoption compared to GPU programming. At this point, the compilation pipeline is complete: the original high-level model representation has been transformed into an optimized, executable format tailored for efficient execution on the target hardware. The combination of graph transformations, kernel selection, memory-aware execution, and parallel scheduling ensures that AI accelerators run workloads with maximum efficiency, minimal memory overhead, and optimal computational throughput. @@ -4565,7 +4568,7 @@ The transition from single-chip to multi-chip architectures introduces qualitati \index{Amdahl's Law!distributed training limit} Communication overhead emerges as the primary limit on scaling efficiency. Amdahl's Law[^fn-amdahls-law-distributed] quantifies how communication during gradient synchronization creates sequential bottlenecks. For hundred-billion-parameter-scale models, AllReduce operations can require exchanging hundreds of gigabytes of gradients per training step. -[^fn-amdahls-law-distributed]: Recall from @sec-ml-systems that Amdahl's Law limits speedup based on the serial fraction of computation. For distributed training, this serial fraction includes gradient synchronization (a core component of distributed training in @sec-model-training-scaling-beyond-single-node-a671), which at 5% overhead caps maximum speedup at 20$\times$ regardless of GPU count. This explains why scaling efficiency degrades at large cluster sizes and motivates algorithmic innovations like gradient compression and communication-computation overlap. +[^fn-amdahls-law-distributed]: Recall from @sec-ml-systems that Amdahl's Law limits speedup based on the serial fraction of computation. For distributed training, this serial fraction includes gradient synchronization (a core component of distributed training in @sec-model-training-scaling-beyond-single-node-a671), which at 5% overhead caps maximum speedup at 20 $\times$ regardless of GPU count. This explains why scaling efficiency degrades at large cluster sizes and motivates algorithmic innovations like gradient compression and communication-computation overlap. This communication overhead explains why scaling to very large accelerator counts can show diminishing returns without algorithmic innovations like gradient compression, overlap, or alternative parallelization strategies. @@ -4586,7 +4589,7 @@ At the edge end of the deployment spectrum, the hardware acceleration principles ::: {.callout-lighthouse title="The Case for Heterogeneous Microcontrollers"} **The Extreme Edge**: The **Smart Doorbell** (Wake Vision) pushes heterogeneity to its logical limit. Unlike a smartphone SoC with a multi-watt budget, a doorbell camera often runs on a microcontroller with a **milliwatt budget**. -To achieve real-time person detection (30 FPS) within this envelope, modern MCUs adopt the same heterogeneous strategy as their larger mobile cousins but at a micro-scale. A typical architecture pairs a general-purpose core (e.g., Cortex-M) for system logic with a dedicated micro-NPU (e.g., Ethos-U\index{Ethos-U!ARM micro-NPU}) for CNN acceleration. The NPU executes the Wake Vision MobileNet model at 50–100$\times$ better energy efficiency than the CPU could achieve alone. Without this specialized acceleration, the "always-on" promise of the Smart Doorbell would remain physically impossible. +To achieve real-time person detection (30 FPS) within this envelope, modern MCUs adopt the same heterogeneous strategy as their larger mobile cousins but at a micro-scale. A typical architecture pairs a general-purpose core (e.g., Cortex-M) for system logic with a dedicated micro-NPU (e.g., Ethos-U\index{Ethos-U!ARM micro-NPU}) for CNN acceleration. The NPU executes the Wake Vision MobileNet model at 50–100 $\times$ better energy efficiency than the CPU could achieve alone. Without this specialized acceleration, the "always-on" promise of the Smart Doorbell would remain physically impossible. ::: ### Mobile SoC Architecture Evolution {#sec-hardware-acceleration-mobile-soc-architecture-evolution-6ca8} @@ -4594,7 +4597,7 @@ To achieve real-time person detection (30 FPS) within this envelope, modern MCUs \index{Mobile SoC!architecture evolution} Qualcomm's Snapdragon AI Engine\index{Snapdragon!AI Engine}\index{Qualcomm!Snapdragon} exemplifies heterogeneous computing\index{Heterogeneous Computing!mobile SoC} for mobile AI, coordinating CPU cores, GPU shaders, a DSP\index{DSP!Digital Signal Processor}, and a dedicated NPU\index{NPU!Neural Processing Unit}[^fn-npu-architecture] across a shared memory hierarchy. Modern mobile SoCs use workload distribution so that computer vision kernels can execute on the GPU's parallel shaders, audio processing can use DSP arithmetic units, and transformer attention mechanisms can utilize NPU-optimized matrix engines. This coordination requires careful scheduling to meet real-time constraints while managing thermal throttling and battery life. -[^fn-npu-architecture]: NPUs (introduced in @sec-ml-systems) excel at low-power inference on common operator patterns but have less programmability than GPUs. From an architecture perspective, NPUs achieve efficiency by hardcoding common dataflows (systolic arrays for matrix multiplication) rather than supporting general computation, trading flexibility for 10–100$\times$ better energy efficiency on supported operations. +[^fn-npu-architecture]: NPUs (introduced in @sec-ml-systems) excel at low-power inference on common operator patterns but have less programmability than GPUs. From an architecture perspective, NPUs achieve efficiency by hardcoding common dataflows (systolic arrays for matrix multiplication) rather than supporting general computation, trading flexibility for 10–100 $\times$ better energy efficiency on supported operations. \index{Unified Memory Architecture!mobile SoC} While Qualcomm's approach emphasizes diverse processor specialization, vertically integrated strategies highlight how tight hardware-software co-design can enable tightly coordinated heterogeneous execution. Unified memory architectures can reduce explicit data copying overhead, and different compute blocks can be scheduled for different operator types (for example, matrix-heavy layers on an NPU, convolutional operators on a GPU, and control flow on the CPU). This coordination supports interactive on-device experiences, though realized latency depends on the full pipeline and device thermal conditions. @@ -4732,7 +4735,7 @@ fp_sync_overhead_str = fmt(sync_overhead_pct, precision=1, commas=False) **Fallacy:** *Hardware acceleration benefits scale linearly with additional accelerators.* -Teams expect 8 GPUs to train 8$\times$ faster than 1 GPU. Multi-accelerator scaling introduces communication overhead that violates linear scaling assumptions. As noted in @sec-hardware-acceleration-multichip-scaling-c649, AllReduce operations for gradient synchronization can require exchanging hundreds of gigabytes per training step for large models. With NVLink at `{python} fp_nvlink_bw_str` GB/s bidirectional, synchronizing 1 GB of gradients requires `{python} fp_sync_time_str` ms; for a 50 ms training step, this represents `{python} fp_sync_overhead_str`% overhead with perfect overlap. Without overlap, 8-GPU setups achieve 7.5$\times$ speedup (94% efficiency) at best, and typical workloads see 6–7$\times$ (75-87% efficiency) due to load imbalance and synchronization barriers. Small models with insufficient parallel work achieve even worse scaling, sometimes seeing 3–4$\times$ speedup on 8 GPUs (37-50% efficiency). +Teams expect 8 GPUs to train 8 $\times$ faster than 1 GPU. Multi-accelerator scaling introduces communication overhead that violates linear scaling assumptions. As noted in @sec-hardware-acceleration-multichip-scaling-c649, AllReduce operations for gradient synchronization can require exchanging hundreds of gigabytes per training step for large models. With NVLink at `{python} fp_nvlink_bw_str` GB/s bidirectional, synchronizing 1 GB of gradients requires `{python} fp_sync_time_str` ms; for a 50 ms training step, this represents `{python} fp_sync_overhead_str`% overhead with perfect overlap. Without overlap, 8-GPU setups achieve 7.5 $\times$ speedup (94% efficiency) at best, and typical workloads see 6–7 $\times$ (75-87% efficiency) due to load imbalance and synchronization barriers. Small models with insufficient parallel work achieve even worse scaling, sometimes seeing 3–4 $\times$ speedup on 8 GPUs (37-50% efficiency). **Fallacy:** *Peak FLOPS specifications determine real-world accelerator performance.* @@ -4784,7 +4787,7 @@ fp_t4_bw_str = fmt(t4_bw, precision=0, commas=False) **Pitfall:** *Deploying small-batch inference workloads on high-compute accelerators.* -Teams deploy high-throughput training accelerators (A100, H100) for latency-sensitive inference with batch size 1–4. As the Roofline Model (@sec-hardware-acceleration-roofline-model-42ff) predicts, small batches severely reduce arithmetic intensity: a dense layer with M=N=2048 achieves AI = `{python} fp_ai_b1_str` FLOP/byte at batch=1 versus AI = `{python} fp_ai_b256_str` FLOP/byte at batch=256. At batch=1, an A100 achieves 4 TFLOPS (1.3% utilization) due to memory bottlenecks, while a lower-cost T4 achieves 3.5 TFLOPS. The T4's peak is `{python} fp_t4_flops_str` TFLOPS (FP16 Tensor Core) with a ridge point of `{python} fp_t4_ridge_str` FLOP/byte (`{python} fp_t4_flops_str` TFLOPS / `{python} fp_t4_bw_str` GB/s). Small-batch inference remains memory-bound on both accelerators, but the T4's lower cost makes it more economical. A100 instances cost 3–4$\times$ more than T4 instances for identical latency. Inference deployments should match batch size to accelerator characteristics, using high-compute accelerators only for batched serving where arithmetic intensity exceeds ridge points. +Teams deploy high-throughput training accelerators (A100, H100) for latency-sensitive inference with batch size 1–4. As the Roofline Model (@sec-hardware-acceleration-roofline-model-42ff) predicts, small batches severely reduce arithmetic intensity: a dense layer with M=N=2048 achieves AI = `{python} fp_ai_b1_str` FLOP/byte at batch=1 versus AI = `{python} fp_ai_b256_str` FLOP/byte at batch=256. At batch=1, an A100 achieves 4 TFLOPS (1.3% utilization) due to memory bottlenecks, while a lower-cost T4 achieves 3.5 TFLOPS. The T4's peak is `{python} fp_t4_flops_str` TFLOPS (FP16 Tensor Core) with a ridge point of `{python} fp_t4_ridge_str` FLOP/byte (`{python} fp_t4_flops_str` TFLOPS / `{python} fp_t4_bw_str` GB/s). Small-batch inference remains memory-bound on both accelerators, but the T4's lower cost makes it more economical. A100 instances cost 3–4 $\times$ more than T4 instances for identical latency. Inference deployments should match batch size to accelerator characteristics, using high-compute accelerators only for batched serving where arithmetic intensity exceeds ridge points. **Pitfall:** *Vendor-specific optimizations without considering long-term portability.* @@ -4878,7 +4881,7 @@ Beyond raw performance, we must evaluate hardware through the lens of *silicon s * **CPU Inference**: 100 Watts for 1 TFLOP (Efficiency = 0.01 TFLOPS/W). * **NPU Inference**: 5 Watts for 10 TFLOPS (Efficiency = 2.0 TFLOPS/W). -* **The Gap**: The NPU is **200$\times$ more energy-efficient** per operation. +* **The Gap**: The NPU is **200 $\times$ more energy-efficient** per operation. **The Calculation**: @@ -4906,7 +4909,7 @@ Building on these foundational concepts, the emergence of multi-chip and distrib * **Memory bandwidth constrains performance**: GPU compute capacity has grown orders of magnitude faster than memory bandwidth over the past two decades. Most inference workloads are memory-bound, making data movement optimization the primary concern. -* **Hardware-software co-design can achieve 10–100$\times$ performance improvements**: Matching algorithm patterns to architectural capabilities (systolic arrays for dense GEMM, sparse accelerators for pruned models) typically outperforms raw hardware upgrades. +* **Hardware-software co-design can achieve 10–100 $\times$ performance improvements**: Matching algorithm patterns to architectural capabilities (systolic arrays for dense GEMM, sparse accelerators for pruned models) typically outperforms raw hardware upgrades. * **Tensor Cores require specific conditions**: FP16 inputs, appropriate tensor dimensions, and sufficient batch size are necessary for peak utilization. Batch size directly affects arithmetic intensity and determines whether workloads reach the compute-bound regime. diff --git a/book/quarto/contents/vol1/introduction/introduction.qmd b/book/quarto/contents/vol1/introduction/introduction.qmd index 079e45e617..31deb39ae5 100644 --- a/book/quarto/contents/vol1/introduction/introduction.qmd +++ b/book/quarto/contents/vol1/introduction/introduction.qmd @@ -244,7 +244,7 @@ imagenet_test_images_str = VerificationGap.imagenet_test_images_str ::: {.callout-perspective title="The Verification Gap"} ***Why* we cannot just 'test' ML systems**\index{Verification Gap}: In Software 1.0, logic is discrete. We can write unit tests that cover edge cases because the input space is often enumerable or partitionable. -In Software 2.0, the input space is **high-dimensional** (e.g., all possible images). Although technically discrete, it is so vast that it is practically unsamplable. Consider an image classifier: a 224$\times$224 RGB image has $256^{150{,}528}$ possible pixel configurations, a number with over `{python} vg_digits_str` digits. ImageNet's entire test set covers only `{python} imagenet_test_images_str` of them. Let **Total Input Space** denote the number of possible inputs and **Test Set Coverage** denote the number of inputs a test suite actually evaluates. No test suite can sample this space meaningfully. @eq-verification-gap captures this disparity: +In Software 2.0, the input space is **high-dimensional** (e.g., all possible images). Although technically discrete, it is so vast that it is practically unsamplable. Consider an image classifier: a 224 $\times$ 224 RGB image has $256^{150{,}528}$ possible pixel configurations, a number with over `{python} vg_digits_str` digits. ImageNet's entire test set covers only `{python} imagenet_test_images_str` of them. Let **Total Input Space** denote the number of possible inputs and **Test Set Coverage** denote the number of inputs a test suite actually evaluates. No test suite can sample this space meaningfully. @eq-verification-gap captures this disparity: $$ \text{Verification Gap} = \text{Total Input Space} - \text{Test Set Coverage} \approx \text{Total Input Space} $$ {#eq-verification-gap} @@ -273,7 +273,7 @@ AI's evolution reveals a progression of bottlenecks, each overcome by systems in [^fn-eliza]: **ELIZA**: Created by MIT's Joseph Weizenbaum in 1966 [@weizenbaum1966eliza], ELIZA simulated conversation via pattern matching. From a systems perspective, it was computationally cheap (running on 256 KB mainframes) but brittle, having no learning capability and no memory of past interactions. -[^fn-mainframes]: **Mainframes**: Room-sized computers that dominated the 1960s-70s. IBM's System/360 (1964) weighed up to 20,000 pounds with ~1 MB of memory, yet represented the cutting edge that enabled early AI research. +[^fn-mainframes]: **Mainframes**: Room-sized computers that dominated the 1960s-70s. IBM's System/360 (1964) weighed up to 9,000 kg (20,000 lbs) with ~1 MB of memory, yet represented the cutting edge that enabled early AI research. The timeline below reveals a recurring pattern: periods of intense optimism followed by "AI winters" when funding collapsed, each triggered by systems limitations that algorithms alone could not overcome. @fig-ai-timeline captures this boom-and-bust rhythm across seven decades: notice how each winter arrives precisely when the dominant paradigm hits its systems ceiling, and each resurgence follows a breakthrough in engineering infrastructure rather than in algorithms alone. Each era represents a paradigm shift attempting to overcome the limitations of the previous approach. @@ -546,7 +546,7 @@ This hybrid approach combined human-engineered features with statistical learnin 4. Post-processing ::: -[^fn-viola-jones]: **Viola-Jones Algorithm**: A groundbreaking computer vision algorithm that detected faces in real-time by using simple rectangular patterns (comparing brightness of eye regions versus cheek regions) and making decisions in stages, filtering out non-faces quickly. The cascade approach reduced computation 10–100$\times$ by rejecting easy negatives early, making real-time vision feasible on CPUs. This compute-saving pattern appears throughout edge ML systems where power budgets matter. +[^fn-viola-jones]: **Viola-Jones Algorithm**: A groundbreaking computer vision algorithm that detected faces in real-time by using simple rectangular patterns (comparing brightness of eye regions versus cheek regions) and making decisions in stages, filtering out non-faces quickly. The cascade approach reduced computation 10–100 $\times$ by rejecting easy negatives early, making real-time vision feasible on CPUs. This compute-saving pattern appears throughout edge ML systems where power budgets matter. ```{python} #| echo: false @@ -1614,7 +1614,7 @@ Just as time is governed by physics, so is energy. We must add a fourth term to $$ \text{Energy}_{\text{total}} \approx \underbrace{ D_{vol} \times E_{\text{move}} }_{\text{Dominant Term}} + \underbrace{ O \times E_{\text{compute}} }_{\text{Secondary Term}} $$ {#eq-energy-cost} -Crucially, $E_{\text{move}} \gg E_{\text{compute}}$. Moving a byte of data from memory often costs 100$\times$ more energy than performing a floating-point operation on it. The physical reason is that data movement requires charging and discharging wires over macroscopic distances, while arithmetic is performed locally within a processing unit's circuits. Therefore, **minimizing data movement ($D_{vol}$)** is the primary lever for both speed *and* energy efficiency. +Crucially, $E_{\text{move}} \gg E_{\text{compute}}$. Moving a byte of data from memory often costs 100 $\times$ more energy than performing a floating-point operation on it. The physical reason is that data movement requires charging and discharging wires over macroscopic distances, while arithmetic is performed locally within a processing unit's circuits. Therefore, **minimizing data movement ($D_{vol}$)** is the primary lever for both speed *and* energy efficiency. The relationship between time, energy, and data movement forms the central analytical tool of this book. @@ -1821,15 +1821,15 @@ algo_efficiency_max_str = EfficiencyGains.algo_efficiency_max_str moores_speedup_str = EfficiencyGains.moores_speedup_str ``` -The magnitude of efficiency improvements is measurable. Between 2012 and 2019, computational resources needed to train a neural network to achieve AlexNet-level performance on ImageNet classification decreased by approximately `{python} algo_efficiency_max_str`$\times$ [@hernandez2020measuring]. This improvement, which halved every 16 months, outpaced hardware efficiency gains predicted by Moore's Law\index{Moore's Law!comparison to AI scaling}[^fn-moores-law], demonstrating that algorithmic innovation drives efficiency as much as hardware advances. +The magnitude of efficiency improvements is measurable. Between 2012 and 2019, computational resources needed to train a neural network to achieve AlexNet-level performance on ImageNet classification decreased by approximately `{python} algo_efficiency_max_str` $\times$ [@hernandez2020measuring]. This improvement, which halved every 16 months, outpaced hardware efficiency gains predicted by Moore's Law\index{Moore's Law!comparison to AI scaling}[^fn-moores-law], demonstrating that algorithmic innovation drives efficiency as much as hardware advances. -[^fn-moores-law]: **Moore's Law**: Intel co-founder Gordon Moore's 1965 observation [@moore1965cramming] that transistor density doubles approximately every two years. For ML systems, Moore's Law matters because AI compute demand has grown far faster---training compute doubled every 3.4 months from 2012--2019, roughly `{python} moores_speedup_str`$\times$ faster. This gap explains why specialized AI accelerators became necessary; @sec-hardware-acceleration traces this progression from general-purpose CPUs through GPUs to domain-specific architectures. +[^fn-moores-law]: **Moore's Law**: Intel co-founder Gordon Moore's 1965 observation [@moore1965cramming] that transistor density doubles approximately every two years. For ML systems, Moore's Law matters because AI compute demand has grown far faster---training compute doubled every 3.4 months from 2012--2019, roughly `{python} moores_speedup_str` $\times$ faster. This gap explains why specialized AI accelerators became necessary; @sec-hardware-acceleration traces this progression from general-purpose CPUs through GPUs to domain-specific architectures. Simultaneously, the compute used in AI training increased by roughly five orders of magnitude from 2012 to 2018, doubling approximately every 3.4 months [@amodei2018aicompute]. This exponential growth far exceeds Moore's Law and explains why efficiency optimization is not optional. Without it, only the most resource-rich organizations could participate in AI development. These measurements emerge from rigorous empirical methodology that tracked training compute across hundreds of published models; @sec-benchmarking develops the measurement frameworks that enable such systematic analysis of ML system performance. Closing the Systems Gap is the primary objective of this textbook, requiring integrated expertise across the software and hardware stack. -To appreciate the magnitude of these gains, consider the trajectory in @fig-algo-efficiency: starting from AlexNet as the baseline, each successive architecture (VGG, ResNet, MobileNet, EfficientNet) achieves comparable accuracy with dramatically fewer computational resources, culminating in a `{python} algo_efficiency_max_str`$\times$ improvement over just eight years. +To appreciate the magnitude of these gains, consider the trajectory in @fig-algo-efficiency: starting from AlexNet as the baseline, each successive architecture (VGG, ResNet, MobileNet, EfficientNet) achieves comparable accuracy with dramatically fewer computational resources, culminating in a `{python} algo_efficiency_max_str` $\times$ improvement over just eight years. ```{python} #| label: fig-algo-efficiency @@ -2016,10 +2016,10 @@ ax.legend(loc="lower right", fontsize=8) plt.show() ``` -Taken together, these two figures reveal a seeming contradiction that defines the economics of modern AI development: @fig-algo-efficiency shows efficiency improving 44$\times$ while @fig-ai-training-compute-growth shows compute demand growing by seven orders of magnitude. How can both be true? +Taken together, these two figures reveal a seeming contradiction that defines the economics of modern AI development: @fig-algo-efficiency shows efficiency improving 44 $\times$ while @fig-ai-training-compute-growth shows compute demand growing by seven orders of magnitude. How can both be true? ::: {.callout-perspective title="The Efficiency Paradox"} -\index{Efficiency Paradox}This apparent contradiction defines the economics of ML systems engineering. Efficiency gains enabled larger experiments, which demanded more compute, which motivated further efficiency research. Consider: if EfficientNet needs 44$\times$ less compute than AlexNet to reach the same accuracy, organizations invest the savings not into cost reduction but into training *larger* models on *more* data, which is precisely how GPT-3 came to require orders of magnitude more compute than AlexNet despite enormous per-FLOP efficiency gains. This feedback loop, where efficiency enables scale and scale demands efficiency, defines the modern AI engineering landscape. Understanding this dynamic is essential for making informed decisions about where to invest optimization effort. +\index{Efficiency Paradox}This apparent contradiction defines the economics of ML systems engineering. Efficiency gains enabled larger experiments, which demanded more compute, which motivated further efficiency research. Consider: if EfficientNet needs 44 $\times$ less compute than AlexNet to reach the same accuracy, organizations invest the savings not into cost reduction but into training *larger* models on *more* data, which is precisely how GPT-3 came to require orders of magnitude more compute than AlexNet despite enormous per-FLOP efficiency gains. This feedback loop, where efficiency enables scale and scale demands efficiency, defines the modern AI engineering landscape. Understanding this dynamic is essential for making informed decisions about where to invest optimization effort. ::: The specific techniques for achieving these gains (pruning algorithms, quantization strategies, knowledge distillation, neural architecture search, hardware-aware optimization, and efficient training procedures) are developed systematically in @sec-model-compression (algorithmic techniques) and @sec-hardware-acceleration (hardware foundations). @sec-data-engineering addresses data selection through pipeline design and quality optimization. @@ -2161,7 +2161,7 @@ Three production systems illustrate how the same engineering principles produce - **Waymo**\index{High-Stakes Hybrid Deployment!autonomous vehicles}[^fn-waymo] is Alphabet's autonomous vehicle division, operating a fleet of self-driving taxis that must make safety-critical decisions in real-time. Waymo represents the *high-stakes hybrid* deployment pattern: on-vehicle perception models run at the edge with <10 ms latency requirements, while massive cloud infrastructure supports training on petabytes of driving data. -[^fn-waymo]: **Waymo**: Originally the Google Self-Driving Car Project (2009), Waymo became an independent Alphabet subsidiary in 2016. Its vehicles have logged over 20 million miles on public roads and billions of miles in simulation. The system integrates LiDAR, radar, and camera inputs through dozens of neural networks running on custom hardware. +[^fn-waymo]: **Waymo**: Originally the Google Self-Driving Car Project (2009), Waymo became an independent Alphabet subsidiary in 2016. Its vehicles have logged over 32 million km (20 million miles) on public roads and billions of kilometers in simulation. The system integrates LiDAR, radar, and camera inputs through dozens of neural networks running on custom hardware. - **FarmBeats**\index{Resource-Constrained Edge Deployment!precision agriculture}[^fn-farmbeats] is Microsoft Research's precision agriculture platform, deploying ML models to farms with limited connectivity. FarmBeats represents the *resource-constrained edge* deployment pattern: models under 500 KB run inference on low-power devices using TV white-space bandwidth measured in kilobits per second. @@ -2300,7 +2300,7 @@ Assumptions that hold in traditional software, academic research, or pure mathem **Fallacy:** *Better algorithms automatically produce better systems.* -Engineers assume algorithmic sophistication drives system performance, but this ignores the Iron Law (@sec-introduction-iron-law-ml-systems-c32a). A state-of-the-art Vision Transformer achieves 1-2% higher accuracy than ResNet-50 on ImageNet but requires 4$\times$ the FLOPs and 3$\times$ the memory bandwidth [@dosovitskiy2021image]. In production, a model that is 1% more accurate but violates latency requirements has effectively zero utility. Google's analysis found that only 5% of production ML code is the model itself; the remaining 95% is data pipelines, serving infrastructure, and monitoring [@sculley2015hidden]. A well-engineered system with a simpler model consistently outperforms a state-of-the-art architecture lacking robust infrastructure. +Engineers assume algorithmic sophistication drives system performance, but this ignores the Iron Law (@sec-introduction-iron-law-ml-systems-c32a). A state-of-the-art Vision Transformer achieves 1-2% higher accuracy than ResNet-50 on ImageNet but requires 4 $\times$ the FLOPs and 3 $\times$ the memory bandwidth [@dosovitskiy2021image]. In production, a model that is 1% more accurate but violates latency requirements has effectively zero utility. Google's analysis found that only 5% of production ML code is the model itself; the remaining 95% is data pipelines, serving infrastructure, and monitoring [@sculley2015hidden]. A well-engineered system with a simpler model consistently outperforms a state-of-the-art architecture lacking robust infrastructure. **Pitfall:** *Treating ML systems as traditional software that happens to include a model.* @@ -2399,7 +2399,7 @@ naive_p = AmdahlsPitfall.naive_p **Pitfall:** *Optimizing individual components without considering system interactions.* -Engineers optimize inference latency in isolation, but **Amdahl's Law** governs end-to-end performance. A team reduces model inference from `{python} t_inference_str` ms to `{python} t_inf_new_str` ms, expecting proportional improvement. But preprocessing consumes `{python} t_pre_str` ms and postprocessing adds `{python} t_post_str` ms, so total latency drops only from `{python} total_ms` ms to `{python} new_total_ms` ms: `{python} improv_pct`% improvement rather than the expected `{python} naive_p`%. The D·A·M taxonomy (@tbl-dam-taxonomy) shows that data, algorithms, and machines form interdependent systems where optimizing one component shifts bottlenecks rather than eliminating them. A model requiring 3$\times$ more preprocessing can increase total cost 40% while improving accuracy only 2%. Teams optimizing components independently often find 50-70% of their engineering effort fails to improve end-to-end metrics. +Engineers optimize inference latency in isolation, but **Amdahl's Law** governs end-to-end performance. A team reduces model inference from `{python} t_inference_str` ms to `{python} t_inf_new_str` ms, expecting proportional improvement. But preprocessing consumes `{python} t_pre_str` ms and postprocessing adds `{python} t_post_str` ms, so total latency drops only from `{python} total_ms` ms to `{python} new_total_ms` ms: `{python} improv_pct`% improvement rather than the expected `{python} naive_p`%. The D·A·M taxonomy (@tbl-dam-taxonomy) shows that data, algorithms, and machines form interdependent systems where optimizing one component shifts bottlenecks rather than eliminating them. A model requiring 3 $\times$ more preprocessing can increase total cost 40% while improving accuracy only 2%. Teams optimizing components independently often find 50-70% of their engineering effort fails to improve end-to-end metrics. ```{python} #| echo: false @@ -2473,7 +2473,7 @@ Through the Bitter Lesson and AI's historical evolution, the chapter demonstrate * **ML systems fail silently, and the Degradation Equation quantifies why**: Unlike traditional software that crashes on errors, ML systems degrade as data distributions drift. Use the Degradation Equation to estimate expected accuracy loss over time and to set retraining triggers based on measurable drift. * **The Iron Law decomposes performance into three terms**: Data movement, computation, and overhead each resolve to time; the slowest term dominates. Reducing inference from `{python} t_inference_str` ms to `{python} t_inf_new_str` ms yields only `{python} improv_pct`% end-to-end improvement when preprocessing (`{python} t_pre_str` ms) and postprocessing (`{python} t_post_str` ms) dominate total latency. * **The Bitter Lesson applies**: Scale and compute outperform hand-crafted features. Systems that leverage general methods with more computation consistently outperform specialized approaches long-term. -* **Efficiency and scale co-evolve across the deployment spectrum**: Algorithmic improvements delivered a 44$\times$ compute reduction, yet total AI compute grew by eight orders of magnitude. Deployment context (from cloud to TinyML) determines which efficiency dimension (algorithmic, compute, or data) to prioritize. +* **Efficiency and scale co-evolve across the deployment spectrum**: Algorithmic improvements delivered a 44 $\times$ compute reduction, yet total AI compute grew by eight orders of magnitude. Deployment context (from cloud to TinyML) determines which efficiency dimension (algorithmic, compute, or data) to prioritize. * **Five pillars require integration**: ML systems engineering encompasses Data Engineering, Training Systems, Deployment Infrastructure, Operations and Monitoring, and Ethics and Governance. Teams lacking expertise in any pillar face 60–70% project failure rates. * **Lifecycle and deployment context reshape every design decision**: ML systems require continuous iteration and monitoring, and the dominant bottlenecks shift across cloud, edge, mobile, and TinyML deployments. diff --git a/book/quarto/contents/vol1/ml_ops/ml_ops.qmd b/book/quarto/contents/vol1/ml_ops/ml_ops.qmd index f362275a41..fe733cf4d6 100644 --- a/book/quarto/contents/vol1/ml_ops/ml_ops.qmd +++ b/book/quarto/contents/vol1/ml_ops/ml_ops.qmd @@ -19,7 +19,7 @@ glossary: ops_glossary.json ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{0}{15}{10}{20}{35}{90}{35}{40} +\mlsysstack{0}{15}{10}{20}{35}{90}{35}{30} \end{marginfigure} \index{MLOps!production deployment challenges} @@ -1248,7 +1248,7 @@ Beyond reproducibility, automation enhances model training by reducing manual ef The increasing availability of cloud-based infrastructure has expanded the reach of model training. This connects to the workflow orchestration patterns explored in @sec-ml-workflow, which provide the foundation for managing complex, multi-stage training processes across distributed systems. Cloud providers offer managed services that provision high-performance computing resources, including GPU and TPU accelerators, on demand[^fn-cloud-ml-costs]. Depending on the platform, teams construct their own training workflows or rely on fully managed services such as Vertex AI Fine Tuning [@vertex_ai_fine_tuning], which support automated adaptation of foundation models to new tasks. Hardware availability, regional access restrictions, and cost constraints remain important considerations when designing cloud-based training systems. -[^fn-cloud-ml-costs]: **Cloud ML Training Economics**: Training GPT-3 was estimated to cost at least \$4.6 million according to Lambda Labs calculations based on V100 GPU-hours; actual costs would likely be higher due to distributed training overhead. Official training costs were not disclosed by OpenAI, while fine-tuning typically costs \$100–\$10,000. TPU v4 offers performance-per-dollar improvements ranging from 1.2–1.7$\times$ over A100 GPUs for general workloads, with some large-scale language model training achieving greater savings. Organizations report 60–90% cost savings through spot instances and preemptible VMs compared to on-demand pricing. +[^fn-cloud-ml-costs]: **Cloud ML Training Economics**: Training GPT-3 was estimated to cost at least \$4.6 million according to Lambda Labs calculations based on V100 GPU-hours; actual costs would likely be higher due to distributed training overhead. Official training costs were not disclosed by OpenAI, while fine-tuning typically costs \$100–\$10,000. TPU v4 offers performance-per-dollar improvements ranging from 1.2–1.7 $\times$ over A100 GPUs for general workloads, with some large-scale language model training achieving greater savings. Organizations report 60–90% cost savings through spot instances and preemptible VMs compared to on-demand pricing. To illustrate these integrated practices, consider a data scientist developing a neural network for image classification using a PyTorch notebook. The fastai [@fastai] library is used to simplify model construction and training. The notebook trains the model on a labeled dataset, computes performance metrics, and tunes model configuration parameters. Once validated, the training script is version-controlled and incorporated into a retraining pipeline that is periodically triggered based on data updates or model performance monitoring. @@ -1441,11 +1441,11 @@ This analysis suggests daily retraining is economically optimal for this high-vo @tbl-retraining-sensitivity shows how the optimal interval scales with the square root of costs and inversely with the square root of value and decay rate: -| **Change** | **Effect on $T^*$** | -|:------------------------------|---------------------------:| -| **4$\times$ retraining cost** | 2$\times$ longer interval | -| **4$\times$ query volume** | 2$\times$ shorter interval | -| **4$\times$ decay rate** | 2$\times$ shorter interval | +| **Change** | **Effect on $T^*$** | +|:-------------------------------|----------------------------:| +| **4 $\times$ retraining cost** | 2 $\times$ longer interval | +| **4 $\times$ query volume** | 2 $\times$ shorter interval | +| **4 $\times$ decay rate** | 2 $\times$ shorter interval | : **Retraining Interval Sensitivity.** How parameter changes affect optimal retraining frequency. Doubling query volume halves the optimal interval because degradation costs scale linearly with traffic. Halving retraining costs similarly reduces the interval, while lower drift rates extend it. Systems with high traffic and high per-query value benefit most from frequent retraining automation. {#tbl-retraining-sensitivity} @@ -1609,7 +1609,7 @@ The fastest tier, *immediate rollback* (under 1 minute), addresses critical fail ###### Rollback Testing {.unnumbered} -Rollback procedures that have never been tested will fail when needed, and the failure mode is particularly insidious: the team discovers the gap at 3:00 AM during an active incident, when cognitive load is highest and time pressure is greatest. Untested rollbacks fail for four distinct reasons, each corresponding to a different infrastructure gap. First, the mechanics of switching model versions often involve subtle configuration dependencies — environment variables, feature flag states, routing rules — that work differently under stress than in documentation. Monthly "fire drills" where teams practice rolling back to previous versions expose these gaps before they matter. Second, manual rollback decisions introduce dangerous latency; defining automated thresholds (e.g., "if P99 latency exceeds 2$\times$ baseline for 5 minutes, trigger rollback") removes human reaction time from the critical path. Third, the rolled-back model must produce consistent behavior rather than corrupted predictions from stale caches or outdated feature values — a validation step that is trivial to skip in testing but catastrophic to miss in production. Finally, step-by-step runbook documentation ensures that the person executing the rollback need not be the person who designed it, a property that becomes essential as team sizes grow and on-call rotations widen. +Rollback procedures that have never been tested will fail when needed, and the failure mode is particularly insidious: the team discovers the gap at 3:00 AM during an active incident, when cognitive load is highest and time pressure is greatest. Untested rollbacks fail for four distinct reasons, each corresponding to a different infrastructure gap. First, the mechanics of switching model versions often involve subtle configuration dependencies — environment variables, feature flag states, routing rules — that work differently under stress than in documentation. Monthly "fire drills" where teams practice rolling back to previous versions expose these gaps before they matter. Second, manual rollback decisions introduce dangerous latency; defining automated thresholds (e.g., "if P99 latency exceeds 2 $\times$ baseline for 5 minutes, trigger rollback") removes human reaction time from the critical path. Third, the rolled-back model must produce consistent behavior rather than corrupted predictions from stale caches or outdated feature values — a validation step that is trivial to skip in testing but catastrophic to miss in production. Finally, step-by-step runbook documentation ensures that the person executing the rollback need not be the person who designed it, a property that becomes essential as team sizes grow and on-call rotations widen. ###### Stateful vs. Stateless Rollback {.unnumbered} @@ -1686,7 +1686,7 @@ These tools and practices, along with distributed orchestration frameworks like #### Model Format Optimization {#sec-ml-operations-model-format-optimization-c9d6} -A PyTorch model that achieves state-of-the-art accuracy on a benchmark may serve predictions at 200 ms latency in production — ten times slower than the SLO requires. The gap between research frameworks and production serving is often substantial, and format optimization\index{Model Optimization!format conversion} bridges it. Optimized formats routinely achieve 2–10$\times$ latency improvements over naive deployment by converting models into representations tailored for specific hardware. The inference runtimes and precision strategies detailed in @sec-model-serving-inference-runtime-selection-5eef and @sec-model-serving-precision-selection-serving-55ba provide the technical foundations; this section focuses on the operational workflow. +A PyTorch model that achieves state-of-the-art accuracy on a benchmark may serve predictions at 200 ms latency in production — ten times slower than the SLO requires. The gap between research frameworks and production serving is often substantial, and format optimization\index{Model Optimization!format conversion} bridges it. Optimized formats routinely achieve 2–10 $\times$ latency improvements over naive deployment by converting models into representations tailored for specific hardware. The inference runtimes and precision strategies detailed in @sec-model-serving-inference-runtime-selection-5eef and @sec-model-serving-precision-selection-serving-55ba provide the technical foundations; this section focuses on the operational workflow. ##### Optimization Frameworks {#sec-ml-operations-optimization-frameworks-d013} @@ -1734,7 +1734,7 @@ Production-grade serving frameworks such as TensorFlow Serving [@tensorflow_serv [^fn-tensorflow-serving]: **TensorFlow Serving**: Google's production-grade ML serving system, capable of approximately 100,000 queries per second per CPU core for lightweight models (benchmarked on 16 vCPU Intel Xeon E5). End-to-end throughput depends on model complexity and batch configuration. -[^fn-triton-performance]: **NVIDIA Triton Inference Server**: Can achieve tens of thousands of inferences per second on a single A100 GPU for optimized BERT-base models with dynamic batching, reducing latency by up to 10$\times$ compared to naive serving approaches. Performance varies significantly with model size and batch configuration. Supports concurrent execution of up to 100 different model types. +[^fn-triton-performance]: **NVIDIA Triton Inference Server**: Can achieve tens of thousands of inferences per second on a single A100 GPU for optimized BERT-base models with dynamic batching, reducing latency by up to 10 $\times$ compared to naive serving approaches. Performance varies significantly with model size and batch configuration. Supports concurrent execution of up to 100 different model types. [^fn-kserve-scaling]: **KServe (formerly KFServing)**: Kubernetes-native model serving framework supporting scale-to-zero and rapid autoscaling for individual models. For a single ML Node, KServe provides standardized inference protocol, model versioning, and canary deployments with minimal configuration. At platform scale, organizations like Bloomberg use KServe to serve thousands of models. @@ -1807,7 +1807,7 @@ post_proc_str = LatencyBudget.post_proc_str | **Post-processing** | 5–12% | 10–20 ms | Async processing, result caching | | **Response serialization** | 3–8% | 5–15 ms | Efficient formats (Protobuf, MessagePack) | -**The Engineering Insight**: Model optimization alone often captures less than 50% of the latency opportunity. A model that runs 2$\times$ faster provides only 1.3$\times$ end-to-end improvement if inference is 45% of total latency. +**The Engineering Insight**: Model optimization alone often captures less than 50% of the latency opportunity. A model that runs 2 $\times$ faster provides only 1.3 $\times$ end-to-end improvement if inference is 45% of total latency. Systems thinking demands end-to-end analysis. Apply the **D·A·M taxonomy** to diagnose the root cause: @@ -2466,7 +2466,7 @@ monitoring_roi_str = fmt(monitoring_roi, precision=0, commas=False) If average incident costs USD `{python} avg_incident_cost_str` (downtime + engineering time + reputation) and monitoring prevents `{python} incidents_prevented_str` incidents annually at USD `{python} annual_monitoring_cost_str` monitoring cost: -ROI = `{python} incidents_prevented_str` $\times$ USD `{python} avg_incident_cost_str` / USD `{python} annual_monitoring_cost_str` = `{python} monitoring_roi_str`$\times$ +ROI = `{python} incidents_prevented_str` $\times$ USD `{python} avg_incident_cost_str` / USD `{python} annual_monitoring_cost_str` = `{python} monitoring_roi_str` $\times$ This framework helps justify monitoring investments and prioritize which metrics deserve fine-grained observation versus coarse sampling. @@ -2739,7 +2739,7 @@ plt.show() Incident communication presents another critical challenge. When models degrade or require rollbacks, maintaining stakeholder trust depends on clear categorization: temporary performance fluctuations as normal variation, data drift as planned maintenance requirements, and system failures demanding immediate rollback. Regular performance reporting cadences preemptively address reliability concerns. -Resource justification requires translating technical requirements into business value. Rather than requesting "8 A100 GPUs for model training," effective communication frames investments as "infrastructure to reduce experiment cycle time from 2 weeks to 3 days, enabling 4$\times$ faster feature iteration." Timeline estimation must account for realistic proportions: data preparation typically consumes 60% of project duration, model development 25%, and deployment monitoring 15%. +Resource justification requires translating technical requirements into business value. Rather than requesting "8 A100 GPUs for model training," effective communication frames investments as "infrastructure to reduce experiment cycle time from 2 weeks to 3 days, enabling 4 $\times$ faster feature iteration." Timeline estimation must account for realistic proportions: data preparation typically consumes 60% of project duration, model development 25%, and deployment monitoring 15%. ```{python} #| label: fraud-detection-improvement-calc @@ -3042,7 +3042,7 @@ For a model generating \$1M annual revenue with: - `{python} hours_saved_monthly_str` hours/month deployment time saved (at USD `{python} hourly_cost_str`/hr) = USD `{python} time_savings_k` K saved - MLOps investment of USD `{python} mlops_investment_k`K/year -ROI = (USD `{python} incident_savings_k` K + USD `{python} time_savings_k` K) / USD `{python} mlops_investment_k` K = `{python} single_model_roi_str`$\times$ +ROI = (USD `{python} incident_savings_k` K + USD `{python} time_savings_k` K) / USD `{python} mlops_investment_k` K = `{python} single_model_roi_str` $\times$ #### When to Invest More {#sec-ml-operations-invest-14a2} @@ -3090,7 +3090,7 @@ The study yielded 440 nights of data and over 3,400 hours of time-synchronized r The team implemented automated data pipelines for ingestion, cleaning, and preprocessing. Leveraging the Edge Impulse platform[^fn-edge-impulse], they consolidated raw inputs from multiple sources, resolved temporal misalignments, and structured data for downstream development. These workflows address **data dependency debt** patterns by implementing robust versioning and lineage tracking, avoiding unstable dependencies that commonly plague embedded ML systems. -[^fn-edge-impulse]: **Edge Impulse Platform**: End-to-end development platform for machine learning on edge devices, founded in 2019 by Jan Jongboom and Zach Shelby (former ARM executives). The platform enables developers to collect data, train models, and deploy to microcontrollers and edge devices with automated model optimization. Over 70,000 developers use Edge Impulse for embedded ML projects, with the platform supporting 80+ hardware targets and providing automatic model compression achieving 100$\times$ size reduction while maintaining accuracy. +[^fn-edge-impulse]: **Edge Impulse Platform**: End-to-end development platform for machine learning on edge devices, founded in 2019 by Jan Jongboom and Zach Shelby (former ARM executives). The platform enables developers to collect data, train models, and deploy to microcontrollers and edge devices with automated model optimization. Over 70,000 developers use Edge Impulse for embedded ML projects, with the platform supporting 80+ hardware targets and providing automatic model compression achieving 100 $\times$ size reduction while maintaining accuracy. #### Model Development and Evaluation {#sec-ml-operations-model-development-evaluation-102e} @@ -3598,7 +3598,7 @@ The following fallacies and pitfalls capture common errors that waste engineerin **Pitfall:** *Focusing on technical infrastructure while neglecting organizational and process alignment.* -\index{MLOps Pitfalls!organizational neglect}Organizations invest in MLOps platforms expecting tooling to solve deployment problems, but sophisticated infrastructure fails without cultural transformation. MLOps demands coordination between data scientists optimizing for accuracy, engineers prioritizing latency, and business stakeholders focused on impact. A retail company deployed feature stores and model registries but maintained quarterly deployment frequency because data scientists and engineers operated in isolation. Industry surveys indicate that unified ML teams deploy 4–8$\times$ more frequently (weekly or daily versus quarterly) compared to siloed organizations. Time-to-production for new models averages 3–6 months in fragmented organizations but drops to 2–4 weeks with integrated teams. Successful MLOps requires cross-functional teams with unified objectives, shared on-call rotations building empathy across roles, and incentive structures rewarding production reliability alongside model performance. +\index{MLOps Pitfalls!organizational neglect}Organizations invest in MLOps platforms expecting tooling to solve deployment problems, but sophisticated infrastructure fails without cultural transformation. MLOps demands coordination between data scientists optimizing for accuracy, engineers prioritizing latency, and business stakeholders focused on impact. A retail company deployed feature stores and model registries but maintained quarterly deployment frequency because data scientists and engineers operated in isolation. Industry surveys indicate that unified ML teams deploy 4–8 $\times$ more frequently (weekly or daily versus quarterly) compared to siloed organizations. Time-to-production for new models averages 3–6 months in fragmented organizations but drops to 2–4 weeks with integrated teams. Successful MLOps requires cross-functional teams with unified objectives, shared on-call rotations building empathy across roles, and incentive structures rewarding production reliability alongside model performance. **Fallacy:** *Training and serving environments automatically remain consistent once pipelines are established.* diff --git a/book/quarto/contents/vol1/ml_systems/ml_systems.qmd b/book/quarto/contents/vol1/ml_systems/ml_systems.qmd index ff636dd3f5..2a36a77c09 100644 --- a/book/quarto/contents/vol1/ml_systems/ml_systems.qmd +++ b/book/quarto/contents/vol1/ml_systems/ml_systems.qmd @@ -28,7 +28,7 @@ start_chapter("vol1:ml_systems") ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{35}{20}{30}{30}{30}{30}{25}{25} +\mlsysstack{35}{20}{30}{30}{30}{30}{25}{15} \end{marginfigure} _Why does deploying the same model to a phone versus a datacenter demand fundamentally different engineering?_ @@ -124,7 +124,7 @@ class MLSystemsSetup: # ┌── 2. CALCULATION (The Physics) ───────────────────────────────────────── # GPT-3 Petaflop-days calculation using standardized units - gpt3_petaflop_days = (m_gpt3.training_ops / (PFLOPs * SEC_PER_DAY)).magnitude + gpt3_petaflop_days = (m_gpt3.training_ops / (PFLOPs * SEC_PER_DAY)).to_base_units().magnitude # ┌── 3. INVARIANTS (Guardrails) ─────────────────────────────────────────── check(gpt3_petaflop_days >= 3000, f"GPT-3 training should be >=3000 PF-days, got {gpt3_petaflop_days:.0f}") @@ -453,7 +453,7 @@ The Power Wall emerged because thermodynamics limits how much computation can oc $$\text{Power} \propto C \times V^2 \times f \quad \text{where } V \propto f \implies \text{Power} \propto f^3$$ {#eq-power-scaling} -Doubling clock frequency required approximately 8$\times$ more power. The breakdown of this scaling relationship ended the era of "free" speedups via frequency scaling and forced the industry toward the parallelism (multi-core) and specialization (GPUs, TPUs) that defines modern ML. Mobile devices hit hard thermal limits at `{python} mobile_tdp_range_str` W; exceeding this causes "throttling," where the device reduces performance to prevent overheating. This physical limit gives rise to **Mobile ML**: battery-powered devices cannot simply run cloud-scale models locally. +Doubling clock frequency required approximately 8 $\times$ more power. The breakdown of this scaling relationship ended the era of "free" speedups via frequency scaling and forced the industry toward the parallelism (multi-core) and specialization (GPUs, TPUs) that defines modern ML. Mobile devices hit hard thermal limits at `{python} mobile_tdp_range_str` W; exceeding this causes "throttling," where the device reduces performance to prevent overheating. This physical limit gives rise to **Mobile ML**: battery-powered devices cannot simply run cloud-scale models locally. ```{python} #| label: memory-wall-calc @@ -614,7 +614,7 @@ et_energy_ratio_str = EnergyTransmission.ratio_str 1. **Cloud Approach**: $E_{cloud} \approx D_{vol} \times E_{tx}$ = `{python} et_data_mb_str` MB $\times$ `{python} et_tx_energy_str` mJ/MB = **`{python} et_cloud_energy_str` mJ**. 2. **Local Approach**: $E_{local} \approx$ Inference = **`{python} et_local_energy_str` mJ**. -**The Systems Conclusion**: Transmitting raw data is **`{python} et_energy_ratio_str`$\times$ more expensive** than processing it locally. Even if the cloud had infinite speed ($Time \approx 0$), the **Energy Wall** makes cloud offloading physically impossible for always-on battery devices. The "Machine" constraint (Battery) dictates the "Algorithm" choice (TinyML). +**The Systems Conclusion**: Transmitting raw data is **`{python} et_energy_ratio_str` $\times$ more expensive** than processing it locally. Even if the cloud had infinite speed ($Time \approx 0$), the **Energy Wall** makes cloud offloading physically impossible for always-on battery devices. The "Machine" constraint (Battery) dictates the "Algorithm" choice (TinyML). ::: The **Iron Law's** variables interact differently across deployment scenarios. Before examining specific workload archetypes, verify your understanding of these core performance determinants. @@ -652,18 +652,23 @@ To navigate these constraints systematically, we categorize ML workloads into fo These archetypes map naturally to deployment paradigms: **Compute Beasts** and **Sparse Scatter** workloads gravitate toward **Cloud ML** where resources are abundant. **Bandwidth Hogs** span Cloud and Edge depending on latency requirements. **Tiny Constraint** workloads are exclusively **TinyML** territory. To make these abstractions concrete, we anchor each archetype to a specific model that recurs throughout this book as one of *five reference workloads*. \index{archetype!etymology} + [^fn-archetype]: **Archetype**: From Greek *arkhetypon*, combining *arkhe* (beginning, origin) and *typos* (pattern, model). Plato used the term for the original forms from which all instances derive. In ML systems, archetypes represent the primary workload patterns from which all specific models derive their computational characteristics. \index{paradigm!etymology} + [^fn-paradigm]: **Paradigm**: See @sec-introduction for the Kuhnian definition of paradigm shift. In ML systems, deployment paradigms (Cloud, Edge, Mobile, TinyML) represent distinct operational frameworks, each with its own constraints, trade-offs, and engineering practices. \index{latency!etymology} + [^fn-latency-etymology]: **Latency**: See the etymology in @sec-introduction. For ML systems, latency budgets often determine deployment paradigm: sub-10 ms requirements mandate edge deployment, while 100 ms+ tolerances permit cloud inference. \index{bandwidth!etymology} + [^fn-bandwidth]: **Bandwidth**: Originally a radio engineering term from the 1930s describing the width of frequency bands. In computing, it measures data transfer rate (bytes/second). The "Memory Wall" (a constraint quantified during hardware analysis in @sec-hardware-acceleration) describes the growing gap between processor speed and memory bandwidth—historically, compute capability has doubled roughly every `{python} compute_doubling_months_str` months while memory bandwidth has improved only ~`{python} mem_bw_growth_pct_str`% annually, making data movement the dominant bottleneck in modern ML systems. \index{critical path!latency determination} + [^fn-critical-path]: **Critical Path**: From project management and systems engineering, the longest sequence of dependent operations that determines total completion time. In ML systems, the critical path is the chain of computations and data movements that sets end-to-end latency—no operation outside this path can reduce total time, while any delay *on* this path directly increases it. Optimizing the critical path is therefore the only way to improve latency. ```{python} @@ -749,6 +754,8 @@ Throughout this book, we use five Lighthouse Models introduced in @sec-introduct | **MobileNet** | Compute Beast (efficient) | Mobile, edge | | **Keyword Spotting (KWS)** | Tiny Constraint | TinyML, always-on | +::: + To ground the abstract interdependencies of the Iron Law in concrete practice, we analyze the Lighthouse Models introduced in @sec-introduction. The following summaries recap each workload from a systems perspective, connecting them to the specific Iron Law bottlenecks they exemplify. **ResNet-50**\index{ResNet-50!systems characteristics} classifies images into 1,000 categories, processing each image through approximately `{python} resnet_gflops_str` billion floating-point operations using `{python} resnet_params_m_str` million parameters (`{python} resnet_fp32_mb_str` MB at FP32). Used in medical imaging diagnostics, autonomous vehicle perception pipelines, and as the backbone for content moderation systems, its regular, compute-dense structure makes it the canonical benchmark for hardware accelerator performance. @@ -757,14 +764,13 @@ To ground the abstract interdependencies of the Iron Law in concrete practice, w **DLRM**\index{DLRM!memory capacity bound}\index{recommendation systems!DLRM} (Deep Learning Recommendation Model) powers the "You might also like" recommendations on platforms like Meta and Netflix. It maps users and items to embedding vectors stored in tables that can exceed `{python} dlrm_embedding_str` GB, making memory capacity rather than computation the binding constraint. -**MobileNet**\index{MobileNet!depthwise separable convolutions}\index{MobileNet!efficiency gains} runs in smartphone camera apps for real-time photo categorization and on-device visual search. It performs the same image classification task as ResNet but uses depthwise separable convolutions to reduce computation by `{python} mobilenet_flops_reduction_str`$\times$, enabling real-time inference on smartphones at `{python} mobile_tdp_range_str` watts. +**MobileNet**\index{MobileNet!depthwise separable convolutions}\index{MobileNet!efficiency gains} runs in smartphone camera apps for real-time photo categorization and on-device visual search. It performs the same image classification task as ResNet but uses depthwise separable convolutions to reduce computation by `{python} mobilenet_flops_reduction_str` $\times$, enabling real-time inference on smartphones at `{python} mobile_tdp_range_str` watts. **Keyword Spotting (KWS)**\index{Keyword Spotting (KWS)!TinyML archetype} represents the always-on TinyML archetype. Used in applications like Smart Doorbells, it detects wake words ("Ding Dong", "Hello") using a depthwise separable CNN with approximately `{python} kws_params_str` parameters (small variants; the DS-CNN benchmark in MLPerf Tiny uses ~200K) fitting in under `{python} kws_size_kb_str` KB, running continuously at under 1 milliwatt. -**Key insight**: The huge range in compute requirements (20 MFLOPs → 4 GFLOPs) and memory (800 KB → 100 GB) explains why no single deployment paradigm fits all workloads. A keyword spotter runs comfortably on a \$2 microcontroller; a recommendation system requires a warehouse-scale computer. -::: +The huge range in compute requirements (20 MFLOPs → 4 GFLOPs) and memory (800 KB → 100 GB) explains why no single deployment paradigm fits all workloads. A keyword spotter runs comfortably on a \$2 microcontroller; a recommendation system requires a warehouse-scale computer. These five Lighthouse Models will serve as concrete anchors throughout the book, each isolating a distinct system bottleneck that we will revisit in every chapter. -These five Lighthouse Models will serve as concrete anchors throughout the book, each isolating a distinct system bottleneck that we will revisit in every chapter. With the analytical tools (Iron Law, Bottleneck Principle, Workload Archetypes) and reference workloads established, we can now apply them to concrete hardware. The next step translates these abstractions into quantitative engineering decisions by examining how system balance—the interplay of compute, memory, and I/O—varies across real hardware platforms. +With the analytical tools (Iron Law, Bottleneck Principle, Workload Archetypes) and reference workloads established, we can now apply them to concrete hardware. The next step translates these abstractions into quantitative engineering decisions by examining how system balance—the interplay of compute, memory, and I/O—varies across real hardware platforms. ## System Balance and Hardware {#sec-ml-systems-system-balance-hardware-96ab} @@ -818,7 +824,7 @@ These latencies, organized by category in @tbl-latency-numbers, span eight order | **LLM token generation** | `{python} lat_llm_str` | Perceived as "typing speed" | | **Memory** | | | | **L1 cache hit** | `{python} lat_l1_str` | Keep hot data in registers | -| **HBM read (GPU)** | `{python} lat_hbm_str` | 100$\times$ slower than compute | +| **HBM read (GPU)** | `{python} lat_hbm_str` | 100 $\times$ slower than compute | | **DRAM read (mobile)** | `{python} lat_dram_str` | Memory-bound on most devices | | **Network** | | | | **Same datacenter** | `{python} lat_net_dc_str` | Microservices feasible | @@ -1036,7 +1042,7 @@ inference_speed_x_str = fmt(inference_speed_x_value, precision=0, commas=False) - Memory bandwidth: `{python} a100_bw_tbs_str` TB/s (HBM2e) - Compute time: $T_{\text{comp}}$ = `{python} cloud_compute_frac` - Memory time: $T_{\text{mem}}$ = `{python} cloud_memory_frac` -- **Bottleneck**: `{python} cloud_bottleneck_str` (`{python} cloud_ratio_x_str`$\times$ slower than compute) +- **Bottleneck**: `{python} cloud_bottleneck_str` (`{python} cloud_ratio_x_str` $\times$ slower than compute) - **Arithmetic Intensity**: `{python} cloud_ai_frac` — this ratio of compute operations to bytes loaded measures how efficiently a workload uses the hardware. When arithmetic intensity exceeds the hardware's *compute-to-bandwidth ratio* ($R_{peak}/BW$), the workload is compute-bound; below it, the workload is memory-bound. For single-image inference, the low batch size yields low arithmetic intensity, explaining why even powerful GPUs are memory-bound at batch=1. **(b) Mobile: Flagship NPU (batch=1, INT8)** @@ -1046,16 +1052,16 @@ inference_speed_x_str = fmt(inference_speed_x_value, precision=0, commas=False) - Model size: `{python} resnet_int8_mb_str` MB (INT8 quantized) - Compute time: $T_{\text{comp}}$ = `{python} mobile_compute_frac` - Memory time: $T_{\text{mem}}$ = `{python} mobile_memory_frac` -- **Bottleneck**: `{python} mobile_bottleneck_str` (`{python} mobile_ratio_x_str`$\times$ slower than compute) +- **Bottleneck**: `{python} mobile_bottleneck_str` (`{python} mobile_ratio_x_str` $\times$ slower than compute) -**Key Insight**\index{quantization!deployment benefits}: Both platforms are memory-bound for single-image inference! The A100's faster memory bandwidth (`{python} a100_bw_tbs_str` TB/s vs `{python} mobile_bw_gbs_str` GB/s = `{python} bw_advantage_x_str`$\times$) translates to roughly `{python} inference_speed_x_str`$\times$ faster inference, not the 10,000$\times$ compute advantage. This explains why quantization (reducing bytes) often beats faster hardware (increasing FLOPS) for deployment. +**Key Insight**\index{quantization!deployment benefits}: Both platforms are memory-bound for single-image inference! The A100's faster memory bandwidth (`{python} a100_bw_tbs_str` TB/s vs `{python} mobile_bw_gbs_str` GB/s = `{python} bw_advantage_x_str` $\times$) translates to roughly `{python} inference_speed_x_str` $\times$ faster inference, not the 10,000 $\times$ compute advantage. This explains why quantization (reducing bytes) often beats faster hardware (increasing FLOPS) for deployment. **When does ResNet-50 become compute-bound?** Increase batch size until $\frac{\text{Ops}}{\text{Compute}} > \frac{\text{Bytes}}{\text{Memory BW}}$. On A100, this occurs around batch=64, where activations dominate memory traffic and high arithmetic intensity is sustained. ::: As systems transition from Cloud to Edge to TinyML, available resources decrease dramatically. @tbl-representative-systems quantifies this progression with concrete hardware examples: memory drops from 131 TB (cloud) to 520 KB (TinyML), a 250 million-fold reduction, while power budgets span nine orders of magnitude from megawatts to milliwatts[^fn-cost-spectrum]. This resource disparity is most acute on microcontrollers, the primary hardware platform for TinyML, where memory and storage capacities are insufficient for conventional ML models. -[^fn-cost-spectrum]: **ML Hardware Cost Spectrum**: The cost range spans 6 orders of magnitude, from \$10 ESP32-CAM modules to multi-million dollar TPU Pod systems. This 100,000$\times$+ cost difference reflects proportional differences in computational capability, enabling deployment across vastly different economic contexts and use cases, from hobbyist projects to hyperscale cloud infrastructure. +[^fn-cost-spectrum]: **ML Hardware Cost Spectrum**: The cost range spans 6 orders of magnitude, from \$10 ESP32-CAM modules to multi-million dollar TPU Pod systems. This 100,000 $\times$ + cost difference reflects proportional differences in computational capability, enabling deployment across vastly different economic contexts and use cases, from hobbyist projects to hyperscale cloud infrastructure. [^fn-pue]: **Power Usage Effectiveness (PUE)**: Data center efficiency metric measuring total facility power divided by IT equipment power. A PUE of 1.0 represents the theoretical lower bound (not achievable in practice), while 1.1–1.3 indicates highly efficient facilities using advanced cooling and power management. Google's data centers achieve fleet-wide PUE of approximately 1.10 (with some facilities as low as 1.06) compared to industry average of approximately 1.58. @@ -1126,12 +1132,12 @@ tiny_thresh_mw_str = "1" # mW power threshold \endgroup -| **Category** | **Quantitative Thresholds** | -|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Cloud ML** | >`{python} cloud_thresh_tflops_str` TFLOPS compute, real-time video processing, >`{python} cloud_thresh_bw_str` GB/s memory bandwidth, PUE 1.1–1.3, 100–500 ms latency | -| **Edge ML** | ~`{python} edge_thresh_pflops_str` PFLOPS AI compute, >`{python} edge_thresh_bw_str` GB/s memory bandwidth, desktop deployment, local processing | -| **Mobile ML** | `{python} mobile_npu_range_str` TOPS compute, <2 W sustained power, <`{python} mobile_latency_range_str` ms UI response | -| **TinyML** | <`{python} tiny_thresh_tops_str` TOPS compute, <`{python} tiny_thresh_mw_str` mW power, microsecond response times | +| **Paradigm** | **Compute** | **Memory BW** | **Power** | **Latency** | +|:--------------|:---------------------------------------------|:-------------------------------------|:----------------------------------|:----------------------------------------| +| **Cloud ML** | >`{python} cloud_thresh_tflops_str` TFLOPS | >`{python} cloud_thresh_bw_str` GB/s | PUE 1.1–1.3 | 100–500 ms | +| **Edge ML** | ~`{python} edge_thresh_pflops_str` PFLOPS AI | >`{python} edge_thresh_bw_str` GB/s | 100s W | `{python} edge_latency_range_str` ms | +| **Mobile ML** | `{python} mobile_npu_range_str` TOPS | `{python} mobile_bw_range_str` GB/s | <2 W | <`{python} mobile_latency_range_str` ms | +| **TinyML** | <`{python} tiny_thresh_tops_str` TOPS | — | <`{python} tiny_thresh_mw_str` mW | µs | : **Deployment Decision Thresholds**: Quantitative thresholds that practitioners use to determine deployment feasibility for each paradigm in @tbl-representative-systems. These numbers answer the practical question "can my workload run here?" by specifying the compute, memory bandwidth, and power envelope that each paradigm provides. {#tbl-deployment-thresholds} @@ -1153,7 +1159,7 @@ Cloud deployments range from single-machine instances (workstations, multi-GPU s [^fn-cloud-evolution]: **Cloud Infrastructure Evolution**: Cloud computing for ML emerged from Amazon's decision in 2002 to treat their internal infrastructure as a service. AWS launched in 2006, followed by Google Cloud Platform (2011) and Google Compute Engine (2012), and Azure (2010). By 2024, worldwide public cloud spending was projected to reach approximately $679 billion [@gartner2024cloud]. -[^fn-nlp-compute]: **NLP Computational Demands**: GPT-3 training cost is estimated at ~\$`{python} gpt3_cost_m_str`M at 2020 V100 cloud rates [@brown2020language]; GPT-4 is estimated at 10–100$\times$ more. This exponential scaling drove hyperscaler investment in specialized infrastructure (TPUs, custom ASICs) and raised concerns about AI's environmental impact [@strubell2019energy] and access inequality. +[^fn-nlp-compute]: **NLP Computational Demands**: GPT-3 training cost is estimated at ~\$`{python} gpt3_cost_m_str`M at 2020 V100 cloud rates [@brown2020language]; GPT-4 is estimated at 10–100 $\times$ more. This exponential scaling drove hyperscaler investment in specialized infrastructure (TPUs, custom ASICs) and raised concerns about AI's environmental impact [@strubell2019energy] and access inequality. What unifies these diverse cloud workloads is a single defining trade-off: @@ -1625,7 +1631,7 @@ The economic argument is compelling, but the *physics* argument is decisive: 2. **GPU demand**: Each query requires ~`{python} vi_gpu_ms_str` ms of GPU time. Total: **`{python} vi_gpu_hours_str` GPU-hours/day**. 3. **Data center capacity**: A large data center (~`{python} vi_gpus_dc_str` GPUs) provides 240,000 GPU-hours/day. 4. **Average requirement**: ~**`{python} vi_dc_avg_str` dedicated data centers** just for voice inference. -5. **Peak reality**: Queries cluster in waking hours (~`{python} vi_peak_ratio_str`$\times$ peak-to-average), requiring **~`{python} vi_dc_peak_str` data centers** at peak. +5. **Peak reality**: Queries cluster in waking hours (~`{python} vi_peak_ratio_str` $\times$ peak-to-average), requiring **~`{python} vi_dc_peak_str` data centers** at peak. **The Bandwidth Wall**: Wake-word detection requires *continuous* audio monitoring. If devices streamed audio to the cloud (16 kHz, 16-bit), each transmits ~`{python} vi_audio_kb_str` KB/s. Across `{python} vi_devices_str` billion devices: **`{python} vi_audio_tb_str` TB/s**—a significant fraction of total global internet backbone capacity. @@ -1818,18 +1824,18 @@ bytes_per_pixel_str = BandwidthBottleneck.bytes_per_pixel_str 1. **Raw data rate per camera**: `{python} video_width_str` $\times$ `{python} video_height_str` $\times$ `{python} bytes_per_pixel_str` bytes $\times$ `{python} bb_fps_str` FPS ≈ **`{python} cam_rate_mbs_str` MB/s**. 2. **Total data rate**: `{python} num_cameras_str` cameras $\times$ `{python} cam_rate_mbs_str` MB/s = **`{python} total_rate_gbs_str` GB/s**. 3. **Cloud upload cost**: At USD `{python} egress_cost_str`/GB egress, streaming 24/7 costs **USD `{python} monthly_cost_m_str` M/month**. -4. **Network reality**: Even a dedicated 10 Gbps line (`{python} net_cap_gbs_str` GB/s) cannot carry the load—you need **`{python} bw_short_x_str`$\times$ more bandwidth** than exists. +4. **Network reality**: Even a dedicated 10 Gbps line (`{python} net_cap_gbs_str` GB/s) cannot carry the load—you need **`{python} bw_short_x_str` $\times$ more bandwidth** than exists. -**The Engineering Conclusion**: Physics has made cloud streaming **impossible** for this application. Edge processing is not optional—it is mandatory. An edge server running local inference transmits only defect metadata (~1 KB per detection), reducing bandwidth requirements by **1,000,000$\times$**. +**The Engineering Conclusion**: Physics has made cloud streaming **impossible** for this application. Edge processing is not optional—it is mandatory. An edge server running local inference transmits only defect metadata (~1 KB per detection), reducing bandwidth requirements by **1,000,000 $\times$**. ::: -The bandwidth calculation above reveals why edge processing is mandatory for high-volume sensor deployments. For battery-powered edge devices (wireless cameras, drones, wearables), the constraint is even more severe: as "The Energy of Transmission" (@sec-ml-systems-bottleneck-principle-3514) established, radio transmission costs `{python} et_energy_ratio_str`$\times$ more energy than local inference, making cloud offloading physically impossible for battery-powered devices regardless of available bandwidth. +The bandwidth calculation above reveals why edge processing is mandatory for high-volume sensor deployments. For battery-powered edge devices (wireless cameras, drones, wearables), the constraint is even more severe: as "The Energy of Transmission" (@sec-ml-systems-bottleneck-principle-3514) established, radio transmission costs `{python} et_energy_ratio_str` $\times$ more energy than local inference, making cloud offloading physically impossible for battery-powered devices regardless of available bandwidth. ### Edge ML Benefits and Deployment Challenges {#sec-ml-systems-edge-ml-benefits-deployment-challenges-b2d0} \index{Edge ML!distributed processing} \index{Edge ML!deployment challenges} \index{Edge ML!privacy benefits} -Edge ML spans wearables, industrial sensors, and smart home appliances that process data locally[^fn-iot-growth] without depending on central servers. @fig-energy-per-inference quantifies the physical imperative: full-system energy per inference spans eight orders of magnitude across deployment paradigms, from ~10 µJ for a TinyML keyword spotter to ~1 kJ for a cloud LLM query. This 100,000,000$\times$ gap is not an engineering shortcoming to be optimized away—it reflects the irreducible costs of data movement, cooling, and network overhead that separate deployment tiers. Memory bandwidth at 25-100 GB/s enables edge models requiring 100 MB-1 GB parameters. The optimization techniques covered in @sec-model-compression achieve 2–4$\times$ speedup compared to cloud models. Local processing also generates substantial bandwidth savings: processing 1000 camera feeds locally avoids 1 Gbps uplink costs and reduces cloud expenses by $10,000-100,000 annually. +Edge ML spans wearables, industrial sensors, and smart home appliances that process data locally[^fn-iot-growth] without depending on central servers. @fig-energy-per-inference quantifies the physical imperative: full-system energy per inference spans eight orders of magnitude across deployment paradigms, from ~10 µJ for a TinyML keyword spotter to ~1 kJ for a cloud LLM query. This 100,000,000 $\times$ gap is not an engineering shortcoming to be optimized away—it reflects the irreducible costs of data movement, cooling, and network overhead that separate deployment tiers. Memory bandwidth at 25-100 GB/s enables edge models requiring 100 MB-1 GB parameters. The optimization techniques covered in @sec-model-compression achieve 2–4 $\times$ speedup compared to cloud models. Local processing also generates substantial bandwidth savings: processing 1000 camera feeds locally avoids 1 Gbps uplink costs and reduces cloud expenses by $10,000-100,000 annually. [^fn-iot-growth]: **IoT Device Growth**: Explosive growth from 8.4B connected devices (2017) to projected 25.4B by 2030 [@mckinsey2021iot]. Daily data generation approaches 2.5 quintillion bytes, with 90% requiring real-time processing. Network bandwidth and cloud costs make edge processing economically essential; uploading raw sensor data would cost $10–100 per device monthly. @@ -1920,7 +1926,7 @@ These benefits carry corresponding limitations that compound as deployments scal Security challenges intensify because edge devices are physically accessible: equipment deployed in retail stores or public infrastructure faces tampering risks that centralized datacenters do not, requiring hardware-based protection mechanisms such as secure boot, encrypted storage, and tamper-evident enclosures. Initial deployment costs of $500-2,000 per edge server compound across locations: instrumenting 1,000 sites requires $500,000-2,000,000 upfront, though these capital costs are offset by lower long-term operational expenses compared to equivalent cloud spending. -[^fn-endpoint-constraints]: **Edge Server Constraints**: Edge hardware operates with 10–100$\times$ less memory (1–8 GB vs. 128–1024 GB), storage (2–32 GB vs. petabytes), and compute compared to cloud servers. Power budgets of 5–50 W vs. 500 W+ per server limit accelerator options. These constraints drive specialized model compression, quantization, and architecture search for edge-deployable models. +[^fn-endpoint-constraints]: **Edge Server Constraints**: Edge hardware operates with 10–100 $\times$ less memory (1–8 GB vs. 128–1024 GB), storage (2–32 GB vs. petabytes), and compute compared to cloud servers. Power budgets of 5–50 W vs. 500 W+ per server limit accelerator options. These constraints drive specialized model compression, quantization, and architecture search for edge-deployable models. [^fn-edge-coordination]: **Edge Network Coordination**: Managing distributed edge devices requires sophisticated orchestration to handle the communication complexity of many interconnected nodes. Hierarchical architectures reduce coordination overhead, and specialized frameworks manage models, data, and updates across heterogeneous devices. We examine these operational patterns, including distributed orchestration and model registries, in @sec-ml-operations. @@ -2100,9 +2106,9 @@ cost_ratio_str = EdgeSizing.cost_ratio_str | **Intel NUC + Movidius** | `{python} nuc_tops_str` TOPS | `{python} nuc_power_w_str` W | USD `{python} nuc_cost_str` | USD `{python} nuc_fleet_k_str`,000 | | **Google Coral Dev** | `{python} coral_tops_str` TOPS | `{python} coral_power_w_str` W | USD `{python} coral_cost_str` | USD `{python} coral_fleet_k_str`,000 | -**Decision**: At `{python} req_tflops_str` TFLOPS required and INT8 quantization providing ~`{python} int8_mult_str`$\times$ effective throughput, the Coral Dev Board (`{python} coral_tops_str` TOPS) meets requirements at 1/`{python} cost_ratio_str` the cost of Jetson, with `{python} power_ratio_str`$\times$ lower power consumption. Note: peak TOPS should be derated by ~50% for realistic sustained throughput (due to operator support, data loading, and memory constraints); the `{python} headroom_str`$\times$ engineering headroom partially accounts for this gap. +**Decision**: At `{python} req_tflops_str` TFLOPS required and INT8 quantization providing ~`{python} int8_mult_str` $\times$ effective throughput, the Coral Dev Board (`{python} coral_tops_str` TOPS) meets requirements at 1/`{python} cost_ratio_str` the cost of Jetson, with `{python} power_ratio_str` $\times$ lower power consumption. Note: peak TOPS should be derated by ~50% for realistic sustained throughput (due to operator support, data loading, and memory constraints); the `{python} headroom_str` $\times$ engineering headroom partially accounts for this gap. -**TCO over `{python} years_str` years** (Coral): Hardware USD `{python} coral_fleet_k_str` K + Power (USD `{python} coral_power_w_str`$\times$`{python} stores_str`$\times$`{python} hours_per_year_str` h$\times$`{python} years_str` yr$\times$USD `{python} elec_cost_str`/kWh) = USD `{python} coral_fleet_k_str` K + USD `{python} coral_pwr_k_str` K = **USD `{python} coral_tco_k_str`,000 total** vs. cloud inference at ~USD `{python} cloud_cost_k_str` K. +**TCO over `{python} years_str` years** (Coral): Hardware USD `{python} coral_fleet_k_str` K + Power (USD `{python} coral_power_w_str` $\times$ `{python} stores_str` $\times$ `{python} hours_per_year_str` h $\times$ `{python} years_str` yr $\times$ USD `{python} elec_cost_str`/kWh) = USD `{python} coral_fleet_k_str` K + USD `{python} coral_pwr_k_str` K = **USD `{python} coral_tco_k_str`,000 total** vs. cloud inference at ~USD `{python} cloud_cost_k_str` K. ::: ### Real-Time Industrial and IoT Systems {#sec-ml-systems-realtime-industrial-iot-systems-373a} @@ -2134,13 +2140,13 @@ These applications share a common assumption: the edge device is stationary and Mobile ML addresses this challenge by integrating machine learning directly into portable devices like smartphones and tablets, providing users with real-time, personalized capabilities. This paradigm excels when user privacy, offline operation, and immediate responsiveness matter more than computational sophistication, supporting applications such as voice recognition[^fn-voice-recognition], computational photography[^fn-computational-photography], and health monitoring while maintaining data privacy through on-device computation. These battery-powered devices must balance performance with power efficiency and thermal management, making them suited to frequent, short-duration AI tasks. \index{depthwise separable convolutions!power reduction} -The mobile environment introduces a critical constraint absent from stationary deployments: *energy per inference* becomes a first-order design parameter. In the Iron Law (@eq-iron-law-extended), cloud and edge systems optimize for minimizing $T$—total latency. Mobile systems face an additional constraint: $\text{Energy} = \text{Power} \times T$, and the Power Wall (@eq-power-scaling) caps sustained power at `{python} mobile_tdp_range_str` W. In Archetype terms, a **Compute Beast** workload like image classification must be transformed through architectural efficiency (e.g., depthwise separable convolutions[^fn-depthwise-separable] in MobileNet) to become a **Compute Beast (efficient)**—reducing FLOPs by `{python} mobilenet_flops_reduction_str`$\times$ while preserving accuracy. This is not merely optimization; it represents a qualitative shift in the arithmetic intensity trade-off, accepting lower peak throughput in exchange for sustainable operation within a `{python} mobile_tdp_range_str` W thermal envelope. +The mobile environment introduces a critical constraint absent from stationary deployments: *energy per inference* becomes a first-order design parameter. In the Iron Law (@eq-iron-law-extended), cloud and edge systems optimize for minimizing $T$—total latency. Mobile systems face an additional constraint: $\text{Energy} = \text{Power} \times T$, and the Power Wall (@eq-power-scaling) caps sustained power at `{python} mobile_tdp_range_str` W. In Archetype terms, a **Compute Beast** workload like image classification must be transformed through architectural efficiency (e.g., depthwise separable convolutions[^fn-depthwise-separable] in MobileNet) to become a **Compute Beast (efficient)**—reducing FLOPs by `{python} mobilenet_flops_reduction_str` $\times$ while preserving accuracy. This is not merely optimization; it represents a qualitative shift in the arithmetic intensity trade-off, accepting lower peak throughput in exchange for sustainable operation within a `{python} mobile_tdp_range_str` W thermal envelope. [^fn-voice-recognition]: **Voice Recognition Evolution**: Apple Siri (2011) required cloud processing with 200–500 ms latency and privacy concerns. By 2017, on-device models reduced latency to <50 ms while keeping audio local. Modern NPUs process 16 kHz audio in 20–30 ms using transformer-based models; Google's on-device transcription achieves 95%+ accuracy entirely locally. [^fn-computational-photography]: **Computational Photography**: Combines multiple exposures and ML algorithms to enhance image quality. Google's Night Sight captures 15 frames in 6 seconds, using ML to align and merge them. Portrait mode uses depth estimation ML models to create professional-looking bokeh effects in real-time. -[^fn-depthwise-separable]: **Depthwise Separable Convolutions**: Architectural innovation introduced by MobileNet (2017) that factorizes standard convolutions into depthwise and pointwise operations. For a $D_K \times D_K$ kernel on $M$ input channels producing $N$ outputs, standard convolution costs $D_K^2 \times M \times N$ multiplications, while depthwise separable costs $D_K^2 \times M + M \times N$, yielding 8–9$\times$ reduction for typical parameters. This efficiency enables running vision models within mobile power budgets. +[^fn-depthwise-separable]: **Depthwise Separable Convolutions**: Architectural innovation introduced by MobileNet [@howard2017mobilenets] that factorizes standard convolutions into depthwise and pointwise operations. For a $D_K \times D_K$ kernel on $M$ input channels producing $N$ outputs, standard convolution costs $D_K^2 \times M \times N$ multiplications, while depthwise separable costs $D_K^2 \times M + M \times N$, yielding 8–9 $\times$ reduction for typical parameters. This efficiency enables running vision models within mobile power budgets. We define this paradigm formally as *Mobile ML*. @@ -2327,7 +2333,7 @@ quant_red_str = fmt(quant_reduction_value, precision=0, commas=False) 3. **Thermal Trip**: Within 60 seconds, the hardware reaches the **Thermal Trip Point** ($80^\circ\text{C}$), triggering OS throttling. 4. **The Result**: Your 100 FPS model suddenly drops to **30 FPS** to avoid melting the hardware. -**The Engineering Conclusion**: Quantization from FP32 to INT8 reduces power by approximately `{python} quant_red_str`$\times$, but if the baseline power is `{python} baseline_str` W, you are still at `{python} quant_power_str` W—the absolute limit of the hardware. Physics sets a hard ceiling that no optimization can exceed. +**The Engineering Conclusion**: Quantization from FP32 to INT8 reduces power by approximately `{python} quant_red_str` $\times$, but if the baseline power is `{python} baseline_str` W, you are still at `{python} quant_power_str` W—the absolute limit of the hardware. Physics sets a hard ceiling that no optimization can exceed. ::: ### Mobile ML Benefits and Resource Constraints {#sec-ml-systems-mobile-ml-benefits-resource-constraints-c568} @@ -2337,13 +2343,13 @@ quant_red_str = fmt(quant_reduction_value, precision=0, commas=False) \index{System-on-Chip (SoC)!mobile architecture} Mobile devices exemplify intermediate constraints: `{python} mobile_ram_range_str` GB RAM (varying from mid-range to flagship), `{python} mobile_storage_range_str` storage, `{python} mobile_npu_range_str` TOPS AI compute through Neural Processing Units[^fn-npu] consuming `{python} mobile_tdp_range_str` W power. System-on-Chip architectures[^fn-mobile-soc] integrate computation and memory to minimize energy costs. Memory bandwidth of `{python} mobile_bw_range_str` GB/s limits models to 10–100 MB parameters, requiring the aggressive optimization techniques that @sec-model-compression details. Battery constraints (`{python} phone_battery_str`–22 Wh capacity) make energy optimization critical: 1 W continuous ML processing reduces device lifetime from 24 to 18 hours. Specialized frameworks (TensorFlow Lite[^fn-tflite], Core ML[^fn-coreml]) provide hardware-optimized inference enabling <`{python} mobile_latency_range_str` ms UI response times. -[^fn-mobile-soc]: **Mobile System-on-Chip (SoC)**: Heterogeneous processors integrating CPU, GPU, NPU, ISP, and memory controller on a single die. Apple's A17 Pro (3nm, 19B transistors) delivers 35 TOPS via its 16-core Neural Engine; Qualcomm's Snapdragon 8 Gen 3 delivers approximately 34 TOPS through its Hexagon NPU. SoC integration reduces data movement energy 10–100$\times$ compared to discrete components. +[^fn-mobile-soc]: **Mobile System-on-Chip (SoC)**: Heterogeneous processors integrating CPU, GPU, NPU, ISP, and memory controller on a single die. Apple's A17 Pro (3nm, 19B transistors) delivers 35 TOPS via its 16-core Neural Engine; Qualcomm's Snapdragon 8 Gen 3 delivers approximately 34 TOPS through its Hexagon NPU. SoC integration reduces data movement energy 10–100 $\times$ compared to discrete components. [^fn-npu]: **Neural Processing Unit (NPU)**: Specialized processors optimized for efficient neural network inference on mobile devices. NPUs achieve high inference performance within tight power budgets, enabling on-device AI. We examine NPU architectures and their performance characteristics in @sec-hardware-acceleration. [^fn-tflite]: **TensorFlow Lite**: Google's mobile/embedded ML framework (2017) optimizing models through quantization, pruning, and operator fusion. Supports Android, iOS, Linux, and microcontrollers. Deploys on 4B+ devices running applications from Google Translate (35 MB multilingual model) to on-device speech recognition with <100 ms latency. -[^fn-coreml]: **Core ML**: Apple's on-device ML framework (iOS 11, 2017) with automatic optimization for Apple Silicon. Seamlessly schedules across CPU, GPU, and Neural Engine based on model characteristics. Supports vision, NLP, and audio models from 1 KB--1 GB with compiler optimizations achieving 2–10$\times$ speedups over naive deployment. +[^fn-coreml]: **Core ML**: Apple's on-device ML framework (iOS 11, 2017) with automatic optimization for Apple Silicon. Seamlessly schedules across CPU, GPU, and Neural Engine based on model characteristics. Supports vision, NLP, and audio models from 1 KB--1 GB with compiler optimizations achieving 2–10 $\times$ speedups over naive deployment. Mobile ML excels at delivering responsive, privacy-preserving user experiences. Real-time processing can reach sub-10 ms latency for some tasks, enabling imperceptible response in interactive applications. Stronger privacy properties emerge when sensitive inputs are processed locally—reducing data transmission and central storage—and on-device enclaves such as Apple's Secure Enclave can further protect sensitive computations like biometric processing[^fn-face-detection], though the strength of privacy guarantees ultimately depends on overall system design and threat model. Offline functionality further differentiates mobile from cloud: navigation, translation[^fn-real-time-translation], and media processing all run locally within mobile resource budgets, eliminating network dependency. Personalization rounds out the advantage, because models can exploit on-device signals and user context while keeping raw data local. @@ -2353,7 +2359,7 @@ Mobile ML excels at delivering responsive, privacy-preserving user experiences. These benefits require accepting tight resource constraints. Compared to cloud deployments, mobile applications often operate under much tighter memory, storage, and latency budgets, which constrains model size and batch behavior. Battery life[^fn-mobile-constraints] presents visible user impact, and thermal throttling can materially limit sustained performance: peak NPU throughput is often substantially higher than what is sustainable under prolonged workloads. Development complexity multiplies across platforms, demanding separate implementations and careful performance tuning, while device heterogeneity requires multiple model variants. Deployment friction adds further challenges: app store review processes can take days, slowing iteration compared to cloud workflows. -[^fn-mobile-constraints]: **Mobile Device Constraints**: Flagship phones (12–24 GB RAM, 15–25 W peak power) operate with 10–100$\times$ less resources than cloud servers (256–2048 GB RAM, 200–400 W). Thermal throttling limits sustained performance; battery life requires <500 mW average inference power. These constraints drove innovations in efficient architectures (MobileNet, EfficientNet) and on-device optimization. +[^fn-mobile-constraints]: **Mobile Device Constraints**: Flagship phones (12–24 GB RAM, 15–25 W peak power) operate with 10–100 $\times$ less resources than cloud servers (256–2048 GB RAM, 200–400 W). Thermal throttling limits sustained performance; battery life requires <500 mW average inference power. These constraints drove innovations in efficient architectures (MobileNet, EfficientNet) and on-device optimization. ### Personal Assistant and Media Processing {#sec-ml-systems-personal-assistant-media-processing-98d7} @@ -2378,7 +2384,7 @@ Mobile ML demonstrates that useful intelligence can operate within a `{python} m TinyML [@reddi2022widening] completes the deployment spectrum by pushing intelligence to its physical limits. Devices costing less than $10 and consuming less than 1 milliwatt of power make ubiquitous[^fn-ubiquitous] sensing economically practical at massive scale. This is the exclusive domain of the **Tiny Constraint** Archetype, where the optimization objective shifts from maximizing throughput to minimizing energy per inference. A keyword spotting model consuming 10 µJ per inference can operate for years on a coin-cell battery, achieving million-fold improvements in energy efficiency by trading model capacity for operational longevity. \index{microcontroller development platforms!TinyML} -Where mobile ML requires sophisticated hardware with gigabytes of memory and multi-core processors, TinyML operates on microcontrollers[^fn-microcontrollers-specs] with kilobytes of RAM and single-digit dollar price points [@banbury2021mlperftiny; @lin2020mcunet]. This radical constraint forces an entirely different approach to machine learning deployment, prioritizing ultra-low power consumption and minimal cost over computational sophistication. TinyML systems power applications such as predictive maintenance, environmental monitoring, and simple gesture recognition. The energy gap between TinyML and cloud inference spans six orders of magnitude[^fn-energy-efficiency]—a 1,000,000$\times$ difference that drives entirely different system architectures and deployment models. This extraordinary efficiency enables operation for months or years on limited power sources such as coin-cell batteries[^fn-coin-cell], as exemplified by the device kits in @fig-TinyML-example. These systems deliver actionable insights in remote or disconnected environments where power, connectivity, and maintenance access are impractical. +Where mobile ML requires sophisticated hardware with gigabytes of memory and multi-core processors, TinyML operates on microcontrollers[^fn-microcontrollers-specs] with kilobytes of RAM and single-digit dollar price points [@banbury2021mlperftiny; @lin2020mcunet]. This radical constraint forces an entirely different approach to machine learning deployment, prioritizing ultra-low power consumption and minimal cost over computational sophistication. TinyML systems power applications such as predictive maintenance, environmental monitoring, and simple gesture recognition. The energy gap between TinyML and cloud inference spans six orders of magnitude[^fn-energy-efficiency]—a 1,000,000 $\times$ difference that drives entirely different system architectures and deployment models. This extraordinary efficiency enables operation for months or years on limited power sources such as coin-cell batteries[^fn-coin-cell], as exemplified by the device kits in @fig-TinyML-example. These systems deliver actionable insights in remote or disconnected environments where power, connectivity, and maintenance access are impractical. [^fn-microcontrollers-specs]: **Microcontrollers**: Single-chip computers with integrated CPU, memory, and peripherals, typically operating at 1–100 MHz with 32 KiB–2 MiB RAM. Arduino Uno uses an ATmega328P with 32 KiB flash and 2 KiB RAM, while ESP32 provides WiFi capability with 520 KiB RAM, still thousands of times less than a smartphone. @@ -2493,7 +2499,7 @@ batt_volt_str = EnergyInference.batt_volt_str Energy values represent *full-system energy* (including server CPUs, memory, networking, and cooling overhead), not isolated accelerator compute energy. For example, the A100 GPU alone executes ResNet-50 inference in under 1 ms (~0.3 J), but the full server draws ~1 kW when amortized across queuing, preprocessing, and idle power. -**Key insight**: A TinyML wake-word detector at 10 µJ/inference is **100,000,000$\times$** more energy-efficient than a cloud LLM query. This gap explains why always-on sensing is only practical at the TinyML tier—a smartphone running continuous cloud queries would drain in minutes. +**Key insight**: A TinyML wake-word detector at 10 µJ/inference is **100,000,000 $\times$** more energy-efficient than a cloud LLM query. This gap explains why always-on sensing is only practical at the TinyML tier—a smartphone running continuous cloud queries would drain in minutes. ::: @fig-tiny-ml positions TinyML relative to the other paradigms. The **Characteristics** branch reveals the extreme constraints: milliwatt power and kilobyte memory. These limits enable the **Benefit** of "always-on" sensing that no other paradigm can sustain, but force engineers to solve the **Challenge** of extreme model compression. @@ -2569,7 +2575,7 @@ above=1of $(B2.north east)!0.5!(B3.north west)$](B0){TinyML}; [^fn-on-device-training]: **On-Device Training Constraints**: Microcontrollers (256KiB-2MiB RAM) cannot support full backpropagation through large networks. Alternatives include on-device fine-tuning of final layers, federated learning with local gradient computation, and TinyTL (memory-efficient training using <50KiB). Apple's on-device personalization adapts keyboard predictions without uploading typing data. -[^fn-device-size]: **TinyML Device Scale**: ML-capable chips range from 5$\times$5 mm (Syntiant NDP: 140 µW, 1 MiB SRAM) to full single-board computers (Coral Dev Board Mini: 40$\times$48 mm, 4 TOPS). This 100$\times$ size range reflects diverse deployment needs from implantable medical devices to industrial edge gateways processing multiple sensor streams simultaneously. +[^fn-device-size]: **TinyML Device Scale**: ML-capable chips range from 5 $\times$ 5 mm (Syntiant NDP: 140 µW, 1 MiB SRAM) to full single-board computers (Coral Dev Board Mini: 40 $\times$ 48 mm, 4 TOPS). This 100 $\times$ size range reflects diverse deployment needs from implantable medical devices to industrial edge gateways processing multiple sensor streams simultaneously. TinyML's extreme resource constraints paradoxically enable unique advantages. By avoiding network transmission entirely, TinyML devices achieve the lowest end-to-end latency in the deployment spectrum, enabling rapid local responses for sensing and control loops without communication overhead. This self-sufficiency also transforms the economics of large-scale deployments: when per-node costs drop to single-digit dollars, instrumenting an entire factory floor, farm, or building becomes financially viable in ways that edge or cloud alternatives cannot match. Energy efficiency compounds the economic case, enabling multi-year operation on small batteries or even indefinite operation through energy harvesting. Privacy benefits follow naturally from locality—raw data never leaves the device, reducing transmission risks and simplifying compliance—though on-device processing alone does not automatically provide formal privacy guarantees without additional security mechanisms. @@ -2588,7 +2594,7 @@ Wake-word detection is perhaps the most familiar consumer application of TinyML. Precision agriculture exploits TinyML's economic advantages where traditional solutions prove cost-prohibitive. Deployments can instrument thousands of monitoring points with multi-year battery operation, transmitting summaries instead of raw sensor streams to reduce connectivity costs. \index{wildlife conservation!TinyML monitoring} -Wildlife conservation uses TinyML for remote environmental monitoring. Researchers deploy solar-powered audio sensors consuming 100–500 mW that process continuous audio streams for species identification. By performing local analysis, these systems reduce satellite transmission requirements from 4.3 GB per day to 400 KB of detection summaries, a 10,000$\times$ reduction that makes large-scale deployments of 100–1,000 sensors economically feasible. +Wildlife conservation uses TinyML for remote environmental monitoring. Researchers deploy solar-powered audio sensors consuming 100–500 mW that process continuous audio streams for species identification. By performing local analysis, these systems reduce satellite transmission requirements from 4.3 GB per day to 400 KB of detection summaries, a 10,000 $\times$ reduction that makes large-scale deployments of 100–1,000 sensors economically feasible. Medical wearables push TinyML into healthcare, where the combination of always-on monitoring and on-device privacy proves uniquely valuable. FDA-cleared cardiac monitors achieve 95–98% sensitivity while processing 250–500 ECG samples per second at under 5 mW power consumption. This efficiency enables week-long continuous monitoring versus hours for smartphone-based alternatives, while reducing diagnostic costs from $2,000–5,000 for traditional in-lab studies to under $100 for at-home testing. @@ -2900,7 +2906,7 @@ The following worked example applies this framework step by step to a safety-cri **Walking through the decision framework**: 1. **Privacy**: Vehicle camera data is not transmitted to third parties → No strong privacy constraint. *Could use cloud.* -2. **Latency**: Emergency braking requires <100 ms total response. At 60 mph, a car travels 2.7 meters in 100 ms. +2. **Latency**: Emergency braking requires <100 ms total response. At 100 km/h, a car travels 2.8 meters in 100 ms. - Network latency to cloud: 50-150 ms (variable) → **Fails requirement** - Edge processing: 10-30 ms → **Passes** - *Decision: Cloud eliminated by physics.* @@ -2933,7 +2939,7 @@ Consider a classification problem solvable by either a **Heuristic** (if-then ru 1. **The Heuristic**: 50 lines of code. Near-zero compute cost. Maintenance: ~1 hour/month to update rules. No drift. 2. **The ML System**: 50 lines of model code + 2,000 lines of infrastructure (data pipelines, monitoring, GPU drivers). Maintenance: ~40 hours/month debugging drift and managing infrastructure. -If the ML system provides 95% accuracy and the heuristic provides 90%, is that 5% gain worth a **40$\times$ increase** in complexity? ML systems engineering is the art of minimizing this tax through robust architecture. If you cannot afford the operational cost to maintain model quality over time, the simpler heuristic may be the superior systems choice. +If the ML system provides 95% accuracy and the heuristic provides 90%, is that 5% gain worth a **40 $\times$ increase** in complexity? ML systems engineering is the art of minimizing this tax through robust architecture. If you cannot afford the operational cost to maintain model quality over time, the simpler heuristic may be the superior systems choice. ::: This complexity tax applies to every deployment decision. Before proceeding to hybrid architectures, reflect on how you would make this trade-off in your own systems. @@ -2944,7 +2950,7 @@ The central trade-off is often **Accuracy vs. Complexity**. **Decision Gates** - [ ] **The Baseline**: Have you measured the accuracy of a simple heuristic (regex, logistic regression) before training a Deep Network? -- [ ] **The Infrastructure Cost**: Is the 2% accuracy gain from a Transformer worth the 10$\times$ inference cost and maintenance burden compared to a smaller model? +- [ ] **The Infrastructure Cost**: Is the 2% accuracy gain from a Transformer worth the 10 $\times$ inference cost and maintenance burden compared to a smaller model? ::: Successful deployment balances technical optimization against organizational capability. Paradigm selection extends well beyond technical requirements to encompass team skills, operational capacity, and economic constraints, all constrained by the physical scaling laws we have examined. Operational aspects are detailed in @sec-ml-operations and benchmarking approaches in @sec-benchmarking. In practice, however, the decision framework rarely points to a single winner. Most production systems combine multiple paradigms, training in the cloud, serving at the edge, preprocessing on mobile, to satisfy constraints that no single deployment target can meet alone. @@ -2968,7 +2974,7 @@ Successful deployment balances technical optimization against organizational cap **Progressive Deployment**\index{progressive deployment!model compression}: Models are systematically compressed for deployment across tiers. A large cloud model becomes progressively optimized versions for edge servers, mobile devices, and tiny sensors. Amazon Alexa exemplifies this: wake-word detection uses <1 KB models consuming <1 mW, while complex natural language understanding requires GB+ models in cloud infrastructure. -[^fn-train-serve-split]: **Train-Serve Split Economics**: Training large models can cost $1-10M (GPT-3: estimated ~$4.6M at 2020 V100 cloud rates) but inference costs <$0.01 per query when deployed efficiently [@brown2020language]. This 1,000,000$\times$ cost difference drives the pattern of expensive cloud training with cost-effective edge inference. +[^fn-train-serve-split]: **Train-Serve Split Economics**: Training large models can cost $1-10M (GPT-3: estimated ~$4.6M at 2020 V100 cloud rates) but inference costs <$0.01 per query when deployed efficiently [@brown2020language]. This 1,000,000 $\times$ cost difference drives the pattern of expensive cloud training with cost-effective edge inference. With three integration patterns available, selecting the right one for a given application requires matching the pattern's trade-off profile to the system's dominant constraints. The following *pattern selection guide* summarizes when each pattern applies. @@ -3150,7 +3156,7 @@ Line/.style={line width=1.0pt,black!50,text=black}, This convergence explains why techniques transfer effectively between scales. Cloud-trained models deploy to edge because both training and inference minimize the same loss function—only the compute budget differs. Quantization techniques developed for edge deployment reduce cloud serving costs, and distributed training strategies inform edge model parallelism. -Mobile optimization insights inform cloud efficiency because memory bandwidth constraints appear at every scale. Techniques like operator fusion and activation checkpointing, developed for mobile's tight memory budgets, reduce cloud inference costs by 2-3$\times$ when applied to batch serving. TinyML innovations drive cross-paradigm advances because extreme constraints force genuinely novel algorithmic breakthroughs: binary neural networks, developed for microcontrollers, now accelerate cloud recommendation systems, and sparse attention mechanisms, essential for fitting transformers in kilobytes, reduce cloud training costs. +Mobile optimization insights inform cloud efficiency because memory bandwidth constraints appear at every scale. Techniques like operator fusion and activation checkpointing, developed for mobile's tight memory budgets, reduce cloud inference costs by 2-3 $\times$ when applied to batch serving. TinyML innovations drive cross-paradigm advances because extreme constraints force genuinely novel algorithmic breakthroughs: binary neural networks, developed for microcontrollers, now accelerate cloud recommendation systems, and sparse attention mechanisms, essential for fitting transformers in kilobytes, reduce cloud training costs. The remaining chapters explore each layer: @sec-data-engineering for data pipelines, @sec-model-compression for optimization, and @sec-ml-operations for operational aspects. All of these apply whether you deploy to a TPU Pod or an ESP32. But shared principles also mean shared vulnerabilities: the same operational challenges—data drift, model decay, monitoring—appear at every tier and demand attention before we consider the chapter's remaining lessons. @@ -3182,7 +3188,7 @@ The following fallacies and pitfalls capture architectural mistakes that waste d **Fallacy:** *One deployment paradigm solves all ML problems.* -Physical constraints create hard boundaries that no single paradigm can span. As @sec-ml-systems-system-balance-hardware-96ab establishes, memory bandwidth scales as the square root of chip area (constrained by die perimeter and pin count) while compute scales linearly with die area, producing qualitatively different bottlenecks across paradigms. @tbl-big_vs_tiny quantifies this: cloud ML achieves 100--1000 ms latency while TinyML delivers 1--10 ms, a 100$\times$ difference rooted in speed-of-light limits, not implementation quality. A real-time robotics system requiring sub-10 ms response cannot use cloud inference regardless of optimization, and a billion-parameter language model cannot fit on a microcontroller with 256 KB RAM regardless of quantization. The optimal architecture typically combines paradigms, such as cloud training with edge inference or mobile preprocessing with cloud analysis. +Physical constraints create hard boundaries that no single paradigm can span. As @sec-ml-systems-system-balance-hardware-96ab establishes, memory bandwidth scales as the square root of chip area (constrained by die perimeter and pin count) while compute scales linearly with die area, producing qualitatively different bottlenecks across paradigms. @tbl-big_vs_tiny quantifies this: cloud ML achieves 100--1000 ms latency while TinyML delivers 1--10 ms, a 100 $\times$ difference rooted in speed-of-light limits, not implementation quality. A real-time robotics system requiring sub-10 ms response cannot use cloud inference regardless of optimization, and a billion-parameter language model cannot fit on a microcontroller with 256 KB RAM regardless of quantization. The optimal architecture typically combines paradigms, such as cloud training with edge inference or mobile preprocessing with cloud analysis. A related misconception holds that moving computation closer to the user always reduces latency, ignoring the processing overhead introduced by less powerful edge hardware—a trade-off explored in **Inference Benchmarks** (@sec-benchmarking-inference-benchmarks-2c1f). @@ -3226,11 +3232,11 @@ Compression techniques do not scale indefinitely against physics. Consider a sma - **Light workload** (1 W inference): `{python} low_power_frac` - **Heavy workload** (5 W, common for large on-device models): `{python} high_power_frac` -The 5 W workload also triggers thermal throttling that reduces performance by 40–60 percent. As @sec-ml-systems-mobile-ml-benefits-resource-constraints-c568 establishes, sustained mobile inference cannot exceed 2–3 W without active cooling. Reducing numerical precision (using fewer bits to represent each weight; see @sec-model-compression) cuts power by approximately 4$\times$, but aggressive precision reduction often causes 5–10 percent accuracy loss. Applications requiring continuous inference beyond mobile thermal envelopes remain physically impossible regardless of algorithmic improvements. +The 5 W workload also triggers thermal throttling that reduces performance by 40–60 percent. As @sec-ml-systems-mobile-ml-benefits-resource-constraints-c568 establishes, sustained mobile inference cannot exceed 2–3 W without active cooling. Reducing numerical precision (using fewer bits to represent each weight; see @sec-model-compression) cuts power by approximately 4 $\times$, but aggressive precision reduction often causes 5–10 percent accuracy loss. Applications requiring continuous inference beyond mobile thermal envelopes remain physically impossible regardless of algorithmic improvements. **Fallacy:** *TinyML represents scaled-down mobile ML.* -The difference is qualitative, not just quantitative. As @sec-ml-systems-tinyml-advantages-operational-tradeoffs-2d40 establishes, TinyML microcontrollers provide 256 KB to 1 MB of memory versus mobile devices with 4–12 GB, a 10,000$\times$ difference requiring entirely different algorithms. Mobile ML uses reduced-precision arithmetic with minimal accuracy loss; TinyML requires extreme precision reduction that sacrifices 10–15 percent accuracy for 32$\times$ memory reduction. Mobile devices run models with millions of parameters; TinyML models contain 10,000–100,000 parameters, demanding distinct architectural choices such as specialized lightweight operations designed to minimize multiply-accumulate counts. Power budgets show similar discontinuities: mobile inference consumes 1–5 W, while TinyML targets 1–10 mW for battery-free energy harvesting. These thousand-fold gaps make TinyML a distinct problem class, not a smaller version of mobile ML. Teams that apply mobile optimization techniques directly to TinyML projects discover that quantization from FP32 to INT8 (reducing each weight from 32 bits to 8 bits; see @sec-model-compression) is insufficient when models must fit in 64 KB, forcing complete architectural redesign. +The difference is qualitative, not just quantitative. As @sec-ml-systems-tinyml-advantages-operational-tradeoffs-2d40 establishes, TinyML microcontrollers provide 256 KB to 1 MB of memory versus mobile devices with 4–12 GB, a 10,000 $\times$ difference requiring entirely different algorithms. Mobile ML uses reduced-precision arithmetic with minimal accuracy loss; TinyML requires extreme precision reduction that sacrifices 10–15 percent accuracy for 32 $\times$ memory reduction. Mobile devices run models with millions of parameters; TinyML models contain 10,000–100,000 parameters, demanding distinct architectural choices such as specialized lightweight operations designed to minimize multiply-accumulate counts. Power budgets show similar discontinuities: mobile inference consumes 1–5 W, while TinyML targets 1–10 mW for battery-free energy harvesting. These thousand-fold gaps make TinyML a distinct problem class, not a smaller version of mobile ML. Teams that apply mobile optimization techniques directly to TinyML projects discover that quantization from FP32 to INT8 (reducing each weight from 32 bits to 8 bits; see @sec-model-compression) is insufficient when models must fit in 64 KB, forcing complete architectural redesign. ```{python} #| label: tco-pitfall-calc @@ -3275,7 +3281,7 @@ tco_ratio_str = fmt(tco_ratio_value, precision=0, commas=False) **Pitfall:** *Minimizing computational resources minimizes total cost.* -Teams optimize per-unit resource consumption while ignoring operational overhead and development velocity. As the decision framework in @sec-ml-systems-decision-framework-241f emphasizes, paradigm selection requires evaluating total cost of ownership, not just compute costs. A cloud inference service costing $`{python} cloud_compute_str` monthly in compute appears expensive versus $`{python} edge_hw_str` monthly edge hardware amortization, but edge deployments add network engineering ($`{python} edge_network_str` monthly), hardware maintenance ($`{python} edge_maint_str` monthly), and reliability engineering ($`{python} edge_reliability_str` monthly), totaling $`{python} edge_total_str`---a `{python} tco_ratio_str`$\times$ difference. Development velocity compounds the gap: cloud deployments reaching production in 2 months versus 6 months for custom edge infrastructure represent 4 months of delayed revenue. The optimal cost solution requires total cost of ownership analysis including development time, operational complexity, and opportunity costs, not merely minimizing compute expenses. +Teams optimize per-unit resource consumption while ignoring operational overhead and development velocity. As the decision framework in @sec-ml-systems-decision-framework-241f emphasizes, paradigm selection requires evaluating total cost of ownership, not just compute costs. A cloud inference service costing $`{python} cloud_compute_str` monthly in compute appears expensive versus $`{python} edge_hw_str` monthly edge hardware amortization, but edge deployments add network engineering ($`{python} edge_network_str` monthly), hardware maintenance ($`{python} edge_maint_str` monthly), and reliability engineering ($`{python} edge_reliability_str` monthly), totaling $`{python} edge_total_str`---a `{python} tco_ratio_str` $\times$ difference. Development velocity compounds the gap: cloud deployments reaching production in 2 months versus 6 months for custom edge infrastructure represent 4 months of delayed revenue. The optimal cost solution requires total cost of ownership analysis including development time, operational complexity, and opportunity costs, not merely minimizing compute expenses. ```{python} #| label: amdahl-camera-calc @@ -3327,13 +3333,13 @@ cam_total_opt_str = fmt(cam_total_optimized_ms_value, precision=0, commas=False) **Fallacy:** *Model optimization translates linearly to system speedup.* -**Amdahl's Law**\index{Amdahl's Law!speedup limits}\index{optimization!Amdahl's Law}[^fn-amdahls-law-systems] establishes hard limits that the Bottleneck Principle (@sec-ml-systems-bottleneck-principle-3514) formalizes: $Speedup_{overall} = \frac{1}{(1-p) + \frac{p}{s}}$ where $p$ is the fraction of work that can be improved and $s$ is the speedup of that fraction. Imagine you tap the shutter on a smartphone camera. The image passes through `{python} cam_isp_str` ms of signal processing (auto-exposure, white balance), `{python} cam_ml_str` ms of ML scene classification, and `{python} cam_post_str` ms of post-processing (tone mapping, HDR merge)---`{python} cam_total_str` ms total. You optimize the ML classifier to run 10$\times$ faster (`{python} cam_ml_opt_str` ms instead of `{python} cam_ml_str` ms), but total time drops from `{python} cam_total_str` ms to `{python} cam_total_opt_str` ms---only `{python} cam_speedup_10x_str`$\times$ overall, not 10$\times$. Even eliminating ML entirely ($s = \infty$) achieves only `{python} cam_speedup_inf_str`$\times$ speedup, because the remaining `{python} cam_non_ml_pct_str` percent of the pipeline is untouched. Effective optimization requires profiling the entire pipeline and addressing bottlenecks systematically, because system performance depends on the slowest unoptimized stage. +**Amdahl's Law**\index{Amdahl's Law!speedup limits}\index{optimization!Amdahl's Law}[^fn-amdahls-law-systems] establishes hard limits that the Bottleneck Principle (@sec-ml-systems-bottleneck-principle-3514) formalizes: $Speedup_{overall} = \frac{1}{(1-p) + \frac{p}{s}}$ where $p$ is the fraction of work that can be improved and $s$ is the speedup of that fraction. Imagine you tap the shutter on a smartphone camera. The image passes through `{python} cam_isp_str` ms of signal processing (auto-exposure, white balance), `{python} cam_ml_str` ms of ML scene classification, and `{python} cam_post_str` ms of post-processing (tone mapping, HDR merge)---`{python} cam_total_str` ms total. You optimize the ML classifier to run 10 $\times$ faster (`{python} cam_ml_opt_str` ms instead of `{python} cam_ml_str` ms), but total time drops from `{python} cam_total_str` ms to `{python} cam_total_opt_str` ms---only `{python} cam_speedup_10x_str` $\times$ overall, not 10 $\times$. Even eliminating ML entirely ($s = \infty$) achieves only `{python} cam_speedup_inf_str` $\times$ speedup, because the remaining `{python} cam_non_ml_pct_str` percent of the pipeline is untouched. Effective optimization requires profiling the entire pipeline and addressing bottlenecks systematically, because system performance depends on the slowest unoptimized stage. -[^fn-amdahls-law-systems]: **Amdahl's Law**: Formulated by Gene Amdahl in 1967 [@amdahl1967validity], this law quantifies theoretical speedup when only part of a system can be improved. The formula $S = 1/((1-p) + p/s)$ shows that even infinite speedup ($s \to \infty$) of the parallelizable fraction $p$ cannot exceed $1/(1-p)$. For ML systems, this explains why end-to-end optimization matters: a 10$\times$ faster GPU yields minimal gains if data loading or preprocessing dominates total latency. See @sec-hardware-acceleration for a detailed treatment. +[^fn-amdahls-law-systems]: **Amdahl's Law**: Formulated by Gene Amdahl in 1967 [@amdahl1967validity], this law quantifies theoretical speedup when only part of a system can be improved. The formula $S = 1/((1-p) + p/s)$ shows that even infinite speedup ($s \to \infty$) of the parallelizable fraction $p$ cannot exceed $1/(1-p)$. For ML systems, this explains why end-to-end optimization matters: a 10 $\times$ faster GPU yields minimal gains if data loading or preprocessing dominates total latency. See @sec-hardware-acceleration for a detailed treatment. **Pitfall:** *Assuming more training data always improves deployed model performance.* -\index{scaling laws!data limitations}Three constraints limit data scaling benefits, as the workload archetypes in @sec-ml-systems-analyzing-workloads-cbb8 illustrate. First, model size limits what can be learned: a keyword spotting model with 250K parameters achieves 95% accuracy on 50K samples but only 96.5% on 1M samples, a 0.3% gain for 5$\times$ more data, storage, and labeling cost. The model simply cannot represent more complex patterns. Second, data quality dominates quantity: 1M curated samples often outperform 100M noisy web-scraped samples, because mislabeled examples and misleading patterns degrade performance even as dataset size grows. Third, deployment distribution matters more than training scale: a model trained on 1B web images may perform worse on medical imaging than one trained on 100K domain-specific samples. Teams that maximize dataset scale without analyzing model capacity waste months of labeling effort for negligible accuracy gains. +\index{scaling laws!data limitations}Three constraints limit data scaling benefits, as the workload archetypes in @sec-ml-systems-analyzing-workloads-cbb8 illustrate. First, model size limits what can be learned: a keyword spotting model with 250K parameters achieves 95% accuracy on 50K samples but only 96.5% on 1M samples, a 0.3% gain for 5 $\times$ more data, storage, and labeling cost. The model simply cannot represent more complex patterns. Second, data quality dominates quantity: 1M curated samples often outperform 100M noisy web-scraped samples, because mislabeled examples and misleading patterns degrade performance even as dataset size grows. Third, deployment distribution matters more than training scale: a model trained on 1B web images may perform worse on medical imaging than one trained on 100K domain-specific samples. Teams that maximize dataset scale without analyzing model capacity waste months of labeling effort for negligible accuracy gains. ## Summary {#sec-ml-systems-summary-d75c} @@ -3344,10 +3350,10 @@ This chapter answered a deceptively simple question: *why does the same model de * **Physical constraints are permanent**\index{physical constraints!permanent boundaries}: Speed of light (~36 ms cross-country round-trip), power wall, and memory wall create hard boundaries that engineering cannot overcome—only navigate. * **Identify bottlenecks before optimizing**\index{bottleneck principle!optimization strategy}: The same model is compute-bound in training but memory-bound in inference. The Iron Law and Bottleneck Principle pinpoint which constraint dominates; optimizing the wrong term yields zero speedup. * **Workload archetypes determine deployment feasibility**: A Compute Beast (ResNet-50 training) requires cloud scale; a Tiny Constraint (keyword spotting) requires microcontroller efficiency. The same optimization strategy cannot serve both—match the archetype to the paradigm. -* **The deployment spectrum spans 1,000,000$\times$ in energy**: Cloud (1 kW) to TinyML (1 mW). This gap enables entirely different application classes rather than representing a limitation. +* **The deployment spectrum spans 1,000,000 $\times$ in energy**: Cloud (1 kW) to TinyML (1 mW). This gap enables entirely different application classes rather than representing a limitation. * **Hybrid architectures are prevalent in production systems**\index{hybrid architectures!voice assistant example}: Voice assistants span TinyML (wake-word), Mobile (speech-to-text), and Cloud (language understanding). Rarely does one paradigm suffice; integration patterns (Train-Serve Split, Hierarchical Processing, Progressive Deployment) formalize how paradigms combine. * **Latency budgets reveal feasibility**\index{latency budgets!feasibility analysis}: 100 ms round-trip to cloud eliminates real-time applications; 10 ms edge inference enables them. Apply the decision framework (@fig-mlsys-playbook-flowchart) to filter paradigms by privacy, latency, compute, and cost. -* **System-level speedup obeys Amdahl's Law, not model-level gains**\index{Amdahl's Law!system optimization}: A 10$\times$ faster model yields only 1.37$\times$ system speedup when ML accounts for 30% of the pipeline. Profile the full system before optimizing any component. +* **System-level speedup obeys Amdahl's Law, not model-level gains**\index{Amdahl's Law!system optimization}: A 10 $\times$ faster model yields only 1.37 $\times$ system speedup when ML accounts for 30% of the pipeline. Profile the full system before optimizing any component. * **Universal system principles transfer across paradigms**: Data pipelines, resource management, and system architecture recur at every scale, which is why optimization ideas can migrate from cloud to edge and back again. ::: diff --git a/book/quarto/contents/vol1/ml_workflow/ml_workflow.qmd b/book/quarto/contents/vol1/ml_workflow/ml_workflow.qmd index 7f8eceee77..60447aa820 100644 --- a/book/quarto/contents/vol1/ml_workflow/ml_workflow.qmd +++ b/book/quarto/contents/vol1/ml_workflow/ml_workflow.qmd @@ -49,13 +49,14 @@ from IPython.display import Markdown ::: \noindent +![](images/png/cover_ai_ml_workflow.png){fig-alt="Illustration of the machine learning workflow lifecycle showing interconnected stages from data collection through model deployment and monitoring."} ::: ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{15}{15}{20}{30}{30}{45}{25}{30} +\mlsysstack{15}{15}{20}{30}{30}{45}{25}{25} \end{marginfigure} _Why do you need to see the whole map before walking any single path?_ @@ -515,7 +516,7 @@ The ML lifecycle is not a straight line; it is a spiral of continuous refinement - [ ] **Problem Definition**: Have you defined success metrics that actually map to business value? - [ ] **Data**: Is your data pipeline reproducible? Can you trace a model prediction back to the training data version? - [ ] **Modeling**: Are you iterating fast enough? (The **Iteration Tax** says speed matters as much as quality). -- [ ] **Deployment**: Have you accounted for the **Constraint Propagation Principle**? (A constraint ignored at stage 1 costs 16$\times$ to fix at stage 5). +- [ ] **Deployment**: Have you accounted for the **Constraint Propagation Principle**? (A constraint ignored at stage 1 costs 16 $\times$ to fix at stage 5). ::: \index{Iron Law of Workflow!definition} @@ -769,7 +770,7 @@ bw_reduction_str = BandwidthCompute.bw_reduction_str 2. **Upload Time**: `{python} bw_daily_mb_str` MB / (`{python} bw_upload_mbps_str`/8 MB/s) = `{python} bw_upload_sec_str` seconds ≈ **`{python} bw_upload_hours_str` hours**. 3. **The Constraint**: If the clinic operates for `{python} bw_clinic_hours_str` hours, uploading this data would require **`{python} bw_bandwidth_pct_str`%** of the clinic's total operating time, effectively saturating the connection and blocking all other operations. -**The Engineering Conclusion**: A Cloud-only architecture is too "expensive" in terms of bandwidth. By moving to the edge, you only need to upload *detection summaries* (~`{python} bw_summary_kb_str` KB/patient), reducing bandwidth usage by `{python} bw_reduction_str`$\times$. +**The Engineering Conclusion**: A Cloud-only architecture is too "expensive" in terms of bandwidth. By moving to the edge, you only need to upload *detection summaries* (~`{python} bw_summary_kb_str` KB/patient), reducing bandwidth usage by `{python} bw_reduction_str` $\times$. ::: ### Lab-to-Field Data Gap {#sec-ml-workflow-bridging-laboratory-realworld-data-e5b6} @@ -1402,19 +1403,19 @@ ML workflows introduce counterintuitive complexities that lead teams to apply fa **Fallacy:** *ML development can follow traditional software workflows without modification.* -Engineers assume waterfall or standard agile processes will work for ML projects. In production, ML replaces deterministic specifications with probabilistic optimization, static behavior with dynamic adaptation, and isolated development with continuous feedback loops (@tbl-sw-ml-cycles). Traditional approaches treat requirements as fixed and testing as binary pass/fail, but ML systems require iterative experimentation where problem definitions evolve through exploration. Industry estimates suggest ML projects fail at 2–3$\times$ the rate of traditional software, with 60–80% never reaching deployment. Projects forced into rigid phase gates miss the 4–8 iteration cycles that production-ready systems require. Organizations that adapt workflows to accommodate ML's experimental nature have reported 40–60% shorter time-to-deployment. +Engineers assume waterfall or standard agile processes will work for ML projects. In production, ML replaces deterministic specifications with probabilistic optimization, static behavior with dynamic adaptation, and isolated development with continuous feedback loops (@tbl-sw-ml-cycles). Traditional approaches treat requirements as fixed and testing as binary pass/fail, but ML systems require iterative experimentation where problem definitions evolve through exploration. Industry estimates suggest ML projects fail at 2–3 $\times$ the rate of traditional software, with 60–80% never reaching deployment. Projects forced into rigid phase gates miss the 4–8 iteration cycles that production-ready systems require. Organizations that adapt workflows to accommodate ML's experimental nature have reported 40–60% shorter time-to-deployment. **Pitfall:** *Treating data preparation as a one-time preprocessing step.* -Teams assume they can "finish" data preparation and move on to modeling. In production, data distributions shift continuously. The two-pipeline architecture in @fig-ml-lifecycle shows data and model pipelines running in parallel with continuous feedback, not sequentially. As @sec-ml-workflow-monitoring-maintenance-stage-e79a establishes, data quality decisions cascade through model training, validation, and deployment. Data quality issues account for 60–80% of production ML failures. Recommendation systems see 10–15% of features requiring updates monthly. Models degrade 5–10% within months as distributions shift, requiring emergency retraining that costs 3–5$\times$ more than proactive monitoring. Organizations that build continuous data validation pipelines from the start detect drift within days rather than months, maintaining accuracy within 2–3% of development baselines. +Teams assume they can "finish" data preparation and move on to modeling. In production, data distributions shift continuously. The two-pipeline architecture in @fig-ml-lifecycle shows data and model pipelines running in parallel with continuous feedback, not sequentially. As @sec-ml-workflow-monitoring-maintenance-stage-e79a establishes, data quality decisions cascade through model training, validation, and deployment. Data quality issues account for 60–80% of production ML failures. Recommendation systems see 10–15% of features requiring updates monthly. Models degrade 5–10% within months as distributions shift, requiring emergency retraining that costs 3–5 $\times$ more than proactive monitoring. Organizations that build continuous data validation pipelines from the start detect drift within days rather than months, maintaining accuracy within 2–3% of development baselines. **Fallacy:** *Passing model evaluation means the system is ready for deployment.* -Engineers treat the model development pipeline as the entire workflow, assuming strong evaluation metrics mean the system is complete. The two-pipeline architecture in @fig-ml-lifecycle exposes the blind spot: this mindset ignores half the lifecycle—data pipeline feedback loops, deployment integration, and production monitoring remain unaddressed. The diabetic retinopathy screening case study (@sec-ml-workflow-case-study-diabetic-retinopathy-screening-7d71) demonstrates the gap: the model passed evaluation but required additional validation to handle equipment variations across clinics, operator skill differences, and demographic diversity absent from curated development data. Evaluation metrics measure algorithm quality in isolation; production readiness requires verifying the complete system, including data freshness, preprocessing consistency, latency under load, and failure recovery. By the Constraint Propagation Principle, a deployment-stage discovery costs $2^{5-1} = 16\times$ the effort of catching it during evaluation design. Teams that equate strong evaluation metrics with deployment readiness consistently underestimate the integration effort by 3–5$\times$. +Engineers treat the model development pipeline as the entire workflow, assuming strong evaluation metrics mean the system is complete. The two-pipeline architecture in @fig-ml-lifecycle exposes the blind spot: this mindset ignores half the lifecycle—data pipeline feedback loops, deployment integration, and production monitoring remain unaddressed. The diabetic retinopathy screening case study (@sec-ml-workflow-case-study-diabetic-retinopathy-screening-7d71) demonstrates the gap: the model passed evaluation but required additional validation to handle equipment variations across clinics, operator skill differences, and demographic diversity absent from curated development data. Evaluation metrics measure algorithm quality in isolation; production readiness requires verifying the complete system, including data freshness, preprocessing consistency, latency under load, and failure recovery. By the Constraint Propagation Principle, a deployment-stage discovery costs $2^{5-1} = 16\times$ the effort of catching it during evaluation design. Teams that equate strong evaluation metrics with deployment readiness consistently underestimate the integration effort by 3–5 $\times$. **Pitfall:** *Skipping validation stages to accelerate timelines.* -Teams assume cutting validation time ships faster. In production, the multi-stage validation process exists because each stage catches different failure modes (@sec-ml-workflow-evaluation-validation-stage-b47d). Skipping shadow mode testing causes integration issues with 10–50$\times$ latency spikes (@sec-ml-workflow-validation-production-conditions-a351). Bypassing canary deployment leads to incidents affecting millions of users. Post-deployment fixes cost 10–100$\times$ more than catching issues during validation. Inadequate validation extends time-to-production by 2–5 months through unplanned remediation. A team that "saves" 2 weeks by skipping validation spends 6–8 weeks on emergency remediation. Organizations investing in systematic validation infrastructure reduce production incidents by 60–80% and achieve substantially higher first-deployment success rates. +Teams assume cutting validation time ships faster. In production, the multi-stage validation process exists because each stage catches different failure modes (@sec-ml-workflow-evaluation-validation-stage-b47d). Skipping shadow mode testing causes integration issues with 10–50 $\times$ latency spikes (@sec-ml-workflow-validation-production-conditions-a351). Bypassing canary deployment leads to incidents affecting millions of users. Post-deployment fixes cost 10–100 $\times$ more than catching issues during validation. Inadequate validation extends time-to-production by 2–5 months through unplanned remediation. A team that "saves" 2 weeks by skipping validation spends 6–8 weeks on emergency remediation. Organizations investing in systematic validation infrastructure reduce production incidents by 60–80% and achieve substantially higher first-deployment success rates. **Pitfall:** *Deferring deployment paradigm selection until after model development.* @@ -1432,13 +1433,13 @@ Three quantitative insights from this chapter should guide your engineering deci 2. **4–8 iteration cycles**: The number of complete cycles production-ready ML systems typically require. Of these iterations, 60% are driven by data quality issues, 25% by architecture choices, and 15% by infrastructure problems. Investment in data engineering yields the highest returns. -3. **$2^{N-1}$ cost escalation**: The Constraint Propagation Principle: a constraint discovered at stage $N$ costs roughly $2^{N-1}$ times more to fix than if caught at stage 1. A deployment paradigm mismatch discovered at stage 5 triggers a `{python} cost_factor_str`$\times$ cost multiplier. Early validation pays exponential dividends. +3. **$2^{N-1}$ cost escalation**: The Constraint Propagation Principle: a constraint discovered at stage $N$ costs roughly $2^{N-1}$ times more to fix than if caught at stage 1. A deployment paradigm mismatch discovered at stage 5 triggers a `{python} cost_factor_str` $\times$ cost multiplier. Early validation pays exponential dividends. ::: {.callout-takeaways title="See the Whole Map First"} * **Two pipelines, one system**: Data processing (collection → preparation) and model development (training → deployment) run in parallel, unified by continuous feedback loops. * **ML systems fail differently than traditional software**: They degrade silently through data drift rather than crashing through code errors, requiring continuous statistical monitoring rather than one-time testing. -* **Iteration velocity compounds**: A model starting 5% behind but iterating 10$\times$ faster overtakes the leader, because each cycle improves data, architecture, and hyperparameters simultaneously (the Iteration Tax). +* **Iteration velocity compounds**: A model starting 5% behind but iterating 10 $\times$ faster overtakes the leader, because each cycle improves data, architecture, and hyperparameters simultaneously (the Iteration Tax). * **Stage interfaces are contracts**: Explicit inputs, outputs, and quality invariants at each stage help prevent the 60–70% of ML project failures caused by integration problems. * **Feedback loops span multiple timescales**: Real-time inference monitoring (seconds), batch retraining triggers (days), and strategic model updates (months) all require distinct automation. * **Constraint propagation is exponential across stages**: Deployment constraints (latency, memory) flow backward to model selection; data constraints (volume, quality) flow forward to architecture choices. A constraint discovered at stage $N$ costs $2^{N-1}$ times more to fix than if caught at stage 1. diff --git a/book/quarto/contents/vol1/model_serving/model_serving.qmd b/book/quarto/contents/vol1/model_serving/model_serving.qmd index 03eb547e90..9ee07df917 100644 --- a/book/quarto/contents/vol1/model_serving/model_serving.qmd +++ b/book/quarto/contents/vol1/model_serving/model_serving.qmd @@ -27,7 +27,7 @@ start_chapter("vol1:serving") ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{35}{30}{25}{15}{90}{40}{20}{15} +\mlsysstack{35}{30}{25}{15}{90}{40}{20}{20} \end{marginfigure} _Why does serving invert every optimization priority that made training successful?_ @@ -188,9 +188,9 @@ bf_collapse_latency_s_str = f"{bf_collapse_latency_s_value}" # e.g. "10" second **The Scenario**: An e-commerce recommendation system runs comfortably at `{python} bf_latency_ms_str` ms latency with `{python} bf_qps_normal_str` queries per second (QPS). -**The Event**: On Black Friday, traffic spikes `{python} bf_spike_factor_str`$\times$ to `{python} bf_qps_spike_str` QPS. +**The Event**: On Black Friday, traffic spikes `{python} bf_spike_factor_str` $\times$ to `{python} bf_qps_spike_str` QPS. -**The Failure**: The system does not just slow down `{python} bf_spike_factor_str`$\times$. It **collapses**. Latency hits `{python} bf_collapse_latency_s_str` seconds, then requests start timing out. The servers are 100% utilized, but *useful* throughput drops to near zero because most completed requests have already timed out from the client's perspective. +**The Failure**: The system does not just slow down `{python} bf_spike_factor_str` $\times$. It **collapses**. Latency hits `{python} bf_collapse_latency_s_str` seconds, then requests start timing out. The servers are 100% utilized, but *useful* throughput drops to near zero because most completed requests have already timed out from the client's perspective. **The Physics**: This is Little's Law and queueing theory in action. As utilization approaches 100%, queue lengths grow exponentially, not linearly. The system spends more time managing the queue (context switching, thrashing) than doing useful work. @@ -436,7 +436,7 @@ Latency constraints directly dictate infrastructure costs. Consider a GPU server * Throughput: `{python} throughput_b_rps_str` req/s (quadrupled due to parallel efficiency). * Cost per million queries: **USD `{python} cost_b_str`**. -**The Trade-off:** Reducing latency from `{python} latency_b_ms_str` ms to `{python} latency_a_ms_str` ms increases the hardware bill by **`{python} cost_increase_str`%**. Engineers must quantify whether that `{python} latency_a_ms_str` ms speedup generates enough business value to justify the `{python} cost_ratio_str`$\times$ cost increase. +**The Trade-off:** Reducing latency from `{python} latency_b_ms_str` ms to `{python} latency_a_ms_str` ms increases the hardware bill by **`{python} cost_increase_str`%**. Engineers must quantify whether that `{python} latency_a_ms_str` ms speedup generates enough business value to justify the `{python} cost_ratio_str` $\times$ cost increase. ::: @@ -445,7 +445,7 @@ Most production systems combine both approaches. Common queries hit a cache popu The static-versus-dynamic decision is the first of several architectural choices that shape serving system design. Equally important is *where* the model executes, since deployment context constrains every subsequent optimization. ::: {.callout-perspective title="Looking Ahead: The Rise of Inference-Time Compute (System 2)"} -Traditional serving optimizes for minimizing latency ($L_{\text{lat}} \to 0$). Emerging "Reasoning Models" (like OpenAI o1) invert this goal, deliberately spending more compute cycles ("thinking") to improve answer quality. Individual token generation remains memory-bandwidth-bound, but these models generate far more tokens per request (often 10–100$\times$ more internal reasoning tokens), dramatically increasing the total compute and energy spent per query. The aggregate effect brings "Training-like" compute budgets into the Serving phase, even though each token is still governed by the memory wall. +Traditional serving optimizes for minimizing latency ($L_{\text{lat}} \to 0$). Emerging "Reasoning Models" (like OpenAI o1) invert this goal, deliberately spending more compute cycles ("thinking") to improve answer quality. Individual token generation remains memory-bandwidth-bound, but these models generate far more tokens per request (often 10–100 $\times$ more internal reasoning tokens), dramatically increasing the total compute and energy spent per query. The aggregate effect brings "Training-like" compute budgets into the Serving phase, even though each token is still governed by the memory wall. ::: ### The Spectrum of Serving Architectures {#sec-model-serving-spectrum-serving-architectures-8966} @@ -710,7 +710,7 @@ Every request traverses a multi-stage pipeline designed to maximize hardware thr This architecture serves three functions. First, *concurrency management*: servers use asynchronous event loops or thread pools to handle thousands of concurrent client connections without blocking, ensuring that network I/O wait times do not idle the accelerator. Second, *request transformation*\index{Request Transformation!tensor formats}: the server converts network payloads (JSON/Protobuf) into the specific tensor formats required by the optimized model runtime. Image tensors, for example, can be stored as NCHW[^fn-nchw-nhwc]\index{NCHW!tensor layout} (batch, channels, height, width) or NHWC\index{NHWC!tensor layout} (batch, height, width, channels). PyTorch and TensorRT prefer NCHW because it places channel data contiguously, enabling efficient convolution on GPUs. TensorFlow defaults to NHWC, which is more efficient on CPUs. -[^fn-nchw-nhwc]: **NCHW and NHWC**: These acronyms encode the memory layout order of 4D image tensors: N (batch), C (channels), H (height), W (width). The layout determines which elements are contiguous in memory, with profound performance implications. NCHW places all values for one channel together, enabling vectorized convolution filters to read contiguous memory blocks on GPUs. NHWC interleaves channels at each spatial position, which aligns better with CPU SIMD instructions that process multiple channels simultaneously. The choice dates to framework design decisions: Caffe (2014) established NCHW as the GPU convention; TensorFlow (2015) chose NHWC for CPU compatibility. A format mismatch between client and server silently corrupts inference: the model interprets pixel rows as color channels, producing garbage outputs without raising errors. +[^fn-nchw-nhwc]: **NCHW and NHWC**: These acronyms encode the memory layout order of 4D image tensors: N (batch), C (channels), H (height), W (width). The layout determines which elements are contiguous in memory, with profound performance implications. NCHW places all values for one channel together, enabling vectorized convolution filters to read contiguous memory blocks on GPUs. NHWC interleaves channels at each spatial position, which aligns better with CPU SIMD instructions that process multiple channels simultaneously. The choice dates to framework design decisions: Caffe [@jia2014caffe] established NCHW as the GPU convention; TensorFlow (2015) chose NHWC for CPU compatibility. A format mismatch between client and server silently corrupts inference: the model interprets pixel rows as color channels, producing garbage outputs without raising errors. Third, *model management*: inference servers manage the lifecycle of models, including loading weights into VRAM, managing versioning, and ensuring that warmup inferences are completed before exposing the model to live traffic. @@ -733,9 +733,10 @@ Text-based\index{Serialization!overhead} formats like JSON are ubiquitous but co \index{FlatBuffers!zero-copy serialization} Binary formats like Protocol Buffers[^fn-protobuf] (Protobuf) or FlatBuffers[^fn-flatbuffers] reduce this overhead by designing the wire format to map directly to in-memory data structures. This enables "zero-copy" deserialization in optimal cases, where the network buffer can be used directly without allocating new memory. -[^fn-protobuf]: **Protocol Buffers (Protobuf)**: Google's language-neutral binary serialization format, first developed internally circa 2001 and open-sourced in 2008. "Protocol" refers to the message format specification (the `.proto` schema), while "Buffers" refers to the serialized byte buffers. Protobuf uses a schema-first design: message structures are defined in `.proto` files, then compiled to language-specific code. The binary encoding is 3--10$\times$ more compact than JSON and 20--100$\times$ faster to parse because the schema eliminates runtime type checking. For ML serving, Protobuf's fixed-size integer encoding and packed repeated fields make tensor serialization efficient, though the format still requires a deserialization step (unlike FlatBuffers). +[^fn-protobuf]: **Protocol Buffers (Protobuf)**: Google's language-neutral binary serialization format, first developed internally circa 2001 and open-sourced in 2008. "Protocol" refers to the message format specification (the `.proto` schema), while "Buffers" refers to the serialized byte buffers. Protobuf uses a schema-first design: message structures are defined in `.proto` files, then compiled to language-specific code. The binary encoding is 3--10 $\times$ more compact than JSON and 20--100 $\times$ faster to parse because the schema eliminates runtime type checking. For ML serving, Protobuf's fixed-size integer encoding and packed repeated fields make tensor serialization efficient, though the format still requires a deserialization step (unlike FlatBuffers). \index{FlatBuffers!etymology} + [^fn-flatbuffers]: **FlatBuffers**: Created by Wouter van Oortmerssen at Google and released in 2014. Originally designed for mobile game development where memory allocation and serialization overhead were unacceptable, FlatBuffers stores data in a format that can be accessed directly without parsing or unpacking. The "flat" in the name refers to the flat binary buffer that serves simultaneously as the serialized and in-memory representation. For ML inference, FlatBuffers enables true zero-copy access to tensor metadata: the serving system can read tensor shapes and data pointers directly from the network buffer without allocating new memory, reducing per-request overhead to near zero. TensorFlow Lite uses FlatBuffers as its model format for exactly this reason. #### REST vs gRPC {#sec-model-serving-rest-vs-grpc-c7b7} @@ -746,7 +747,7 @@ In contrast, gRPC (gRPC Remote Procedure Call)\index{gRPC!inference protocol}[^f [^fn-rest]: **REST (Representational State Transfer)**\index{REST!etymology}: Defined by Roy Fielding [@fielding2000rest] in his 2000 PhD dissertation at UC Irvine. An *architectural style* rather than a protocol, REST distills the web's design principles into six constraints: client-server separation, statelessness, cacheability, uniform interface, layered system, and code on demand. Its simplicity made it the dominant paradigm for web APIs, though text-based HTTP/1.1 and JSON serialization create overhead for ML inference, where binary tensor data dominates the payload. -[^fn-grpc]: **gRPC**: Open-sourced by Google in February 2015, gRPC evolved from Stubby, Google's internal RPC framework that had been handling tens of billions of calls per second across their datacenters since approximately 2001. The combination of HTTP/2 multiplexing and Protocol Buffers binary serialization achieves roughly 10$\times$ lower serialization overhead than REST/JSON, making it the de facto standard for latency-sensitive ML inference APIs. +[^fn-grpc]: **gRPC**: Open-sourced by Google in February 2015, gRPC evolved from Stubby, Google's internal RPC framework that had been handling tens of billions of calls per second across their datacenters since approximately 2001. The combination of HTTP/2 multiplexing and Protocol Buffers binary serialization achieves roughly 10 $\times$ lower serialization overhead than REST/JSON, making it the de facto standard for latency-sensitive ML inference APIs. The following example compares *JSON vs Protobuf serialization*. @@ -812,7 +813,7 @@ Consider a request payload containing `{python} serial_floats_str` floating poin * **JSON**: Uses ~`{python} json_size_str` KB on the wire. Requires ~`{python} json_parse_str` μs to parse. * **Protobuf**: Uses ~`{python} protobuf_size_str` KB on the wire. Requires ~`{python} protobuf_parse_str` μs to parse. -For a system processing `{python} requests_per_sec_str` requests per second, switching to Protobuf saves nearly half a core of CPU time just in serialization overhead. This `{python} efficiency_gain_str`$\times$ efficiency gain makes gRPC essential for high-throughput internal microservices. +For a system processing `{python} requests_per_sec_str` requests per second, switching to Protobuf saves nearly half a core of CPU time just in serialization overhead. This `{python} efficiency_gain_str` $\times$ efficiency gain makes gRPC essential for high-throughput internal microservices. ::: @@ -998,7 +999,7 @@ A typical serving request for our ResNet-50 classifier shows the following laten | **Phase** | **Operation** | **Time** | **Percentage** | |:-------------------|:---------------------------|:---------------------------|:--------------------------| | **Preprocessing** | JPEG decode | `{python} l_jpeg_str` | `{python} p_jpeg_str` | -| **Preprocessing** | Resize to 224$\times$224 | `{python} l_resize_str` | `{python} p_resize_str` | +| **Preprocessing** | Resize to 224 $\times$ 224 | `{python} l_resize_str` | `{python} p_resize_str` | | **Preprocessing** | Normalize (mean/std) | `{python} l_norm_str` | `{python} p_norm_str` | | **Data Transfer** | CPU→GPU copy | `{python} l_transfer_str` | `{python} p_transfer_str` | | **Inference** | **ResNet-50 forward pass** | **`{python} l_inf_str`** | **`{python} p_inf_str`** | @@ -1105,16 +1106,16 @@ amdahl_speedup_str = fmt(amdahl_speedup_value, precision=1, commas=False) # ::: {.callout-notebook title="The Quantitative Approach to Serving"} -**Amdahl's Law at Work** (see @sec-machine-foundations-amdahls-law-gustafsons-law-b741 for the formal derivation): preprocessing (`{python} preprocess_ms_str` ms) and data transfer (`{python} cpu_gpu_ms_str` ms) consume `{python} non_model_pct_str`% of total latency. Optimizing the model 10$\times$ faster (`{python} resnet_inference_ms_str` ms → `{python} model_10x_ms_str` ms) yields only `{python} amdahl_speedup_str`$\times$ end-to-end speedup (from `{python} total_latency_str` ms to `{python} optimized_total_str` ms). This is why focusing exclusively on model optimization (quantization, pruning) often disappoints: the bottleneck is elsewhere. +**Amdahl's Law at Work** (see @sec-machine-foundations-amdahls-law-gustafsons-law-b741 for the formal derivation): preprocessing (`{python} preprocess_ms_str` ms) and data transfer (`{python} cpu_gpu_ms_str` ms) consume `{python} non_model_pct_str`% of total latency. Optimizing the model 10 $\times$ faster (`{python} resnet_inference_ms_str` ms → `{python} model_10x_ms_str` ms) yields only `{python} amdahl_speedup_str` $\times$ end-to-end speedup (from `{python} total_latency_str` ms to `{python} optimized_total_str` ms). This is why focusing exclusively on model optimization (quantization, pruning) often disappoints: the bottleneck is elsewhere. -**DSA Efficiency**: General-purpose CPUs achieve only 1–2% of peak performance at batch-1 because instruction overhead dominates. DSAs like TPUs and Tensor Cores replace complex logic with dense MAC arrays, achieving 10–100$\times$ higher arithmetic intensity. This makes hardware acceleration a requirement for economically viable serving. +**DSA Efficiency**: General-purpose CPUs achieve only 1–2% of peak performance at batch-1 because instruction overhead dominates. DSAs like TPUs and Tensor Cores replace complex logic with dense MAC arrays, achieving 10–100 $\times$ higher arithmetic intensity. This makes hardware acceleration a requirement for economically viable serving. **Engineering Implication**: Profile before optimizing. If preprocessing dominates, GPU-accelerated pipelines (NVIDIA DALI) may outperform model quantization. ::: -Moving preprocessing to GPU[^fn-dali]\index{GPU Preprocessing!accelerated pipelines} can reduce total latency by 6$\times$ in some pipelines by eliminating CPU-GPU data transfers between stages [@nvidia_triton]. +Moving preprocessing to GPU[^fn-dali]\index{GPU Preprocessing!accelerated pipelines} can reduce total latency by 6 $\times$ in some pipelines by eliminating CPU-GPU data transfers between stages [@nvidia_triton]. -[^fn-dali]: **NVIDIA DALI (Data Loading Library)**: Released by NVIDIA in 2018, DALI moves image preprocessing operations (decoding, resizing, color space conversion, normalization) from the CPU to the GPU. The name "DALI" evokes the surrealist painter Salvador Dalí, though NVIDIA uses the acronym for Data Loading Library. The key insight is that CPU-based preprocessing becomes the bottleneck when inference runs on optimized accelerators: a V100 can classify an image in under 1 ms, but CPU-based JPEG decoding and resizing may take 3--5 ms. DALI's GPU-accelerated pipeline processes these operations in parallel with inference, achieving 2--6$\times$ end-to-end speedup by eliminating the CPU preprocessing bottleneck identified in Amdahl's Law analysis. For serving, DALI also reduces training-serving skew risk by providing identical preprocessing implementations across both pipelines. +[^fn-dali]: **NVIDIA DALI (Data Loading Library)**: Released by NVIDIA in 2018, DALI moves image preprocessing operations (decoding, resizing, color space conversion, normalization) from the CPU to the GPU. The name "DALI" evokes the surrealist painter Salvador Dalí, though NVIDIA uses the acronym for Data Loading Library. The key insight is that CPU-based preprocessing becomes the bottleneck when inference runs on optimized accelerators: a V100 can classify an image in under 1 ms, but CPU-based JPEG decoding and resizing may take 3--5 ms. DALI's GPU-accelerated pipeline processes these operations in parallel with inference, achieving 2--6 $\times$ end-to-end speedup by eliminating the CPU preprocessing bottleneck identified in Amdahl's Law analysis. For serving, DALI also reduces training-serving skew risk by providing identical preprocessing implementations across both pipelines. Effective optimization targets the largest time consumers first. @@ -1178,7 +1179,7 @@ theoretical_str = fmt(theoretical_slowdown_value, precision=0, commas=False) # e measured_slowdown_str = f"{measured_slowdown_value}" # e.g. "3.6" x ``` -Doubling resolution from `{python} r1_str` to `{python} r2_str` theoretically yields `{python} theoretical_str`$\times$ slowdown (measured: `{python} measured_slowdown_str`$\times$ due to fixed overhead amortization). However, at high resolutions, models transition from compute-bound to memory-bound as activation tensors exceed cache capacity. @tbl-resolution-bottleneck quantifies this transition for ResNet-50, showing how arithmetic intensity decreases with resolution: +Doubling resolution from `{python} r1_str` to `{python} r2_str` theoretically yields `{python} theoretical_str` $\times$ slowdown (measured: `{python} measured_slowdown_str` $\times$ due to fixed overhead amortization). However, at high resolutions, models transition from compute-bound to memory-bound as activation tensors exceed cache capacity. @tbl-resolution-bottleneck quantifies this transition for ResNet-50, showing how arithmetic intensity decreases with resolution: ```{python} #| label: resolution-bottleneck-calc @@ -1227,18 +1228,18 @@ ridge_point_str = f"{ridge_point_value}" # e The resulting shift from compute-bound to memory-bound operation is evident in @tbl-resolution-bottleneck: -| **Resolution** | **Activation Size** | **Arith. Intensity** | **Bottleneck** | -|:-------------------|----------------------------:|---------------------------------:|:---------------| -| **224$\times$224** | `{python} act_224_mb_str`MB | `{python} ai_224_str` FLOPs/byte | Compute | -| **384$\times$384** | `{python} act_384_mb_str`MB | `{python} ai_384_str` FLOPs/byte | Transitional | -| **512$\times$512** | `{python} act_512_mb_str`MB | `{python} ai_512_str` FLOPs/byte | Memory BW | -| **640$\times$640** | `{python} act_640_mb_str`MB | `{python} ai_640_str` FLOPs/byte | Memory BW | +| **Resolution** | **Activation Size** | **Arith. Intensity** | **Bottleneck** | +|:---------------------|----------------------------:|---------------------------------:|:---------------| +| **224 $\times$ 224** | `{python} act_224_mb_str`MB | `{python} ai_224_str` FLOPs/byte | Compute | +| **384 $\times$ 384** | `{python} act_384_mb_str`MB | `{python} ai_384_str` FLOPs/byte | Transitional | +| **512 $\times$ 512** | `{python} act_512_mb_str`MB | `{python} ai_512_str` FLOPs/byte | Memory BW | +| **640 $\times$ 640** | `{python} act_640_mb_str`MB | `{python} ai_640_str` FLOPs/byte | Memory BW | -: **Resolution and Compute Bottleneck**: ResNet-50 arithmetic intensity decreases with resolution as activation sizes grow. For a V100 PCIe (15.7 TFLOPS FP32, 900 GB/s bandwidth), the ridge point is approximately 16 FLOPs/byte. At 224$\times$224, compute dominates; by 512$\times$512, memory bandwidth becomes the limiting factor. {#tbl-resolution-bottleneck} +: **Resolution and Compute Bottleneck**: ResNet-50 arithmetic intensity decreases with resolution as activation sizes grow. For a V100 PCIe (15.7 TFLOPS FP32, 900 GB/s bandwidth), the ridge point is approximately 16 FLOPs/byte. At 224 $\times$ 224, compute dominates; by 512 $\times$ 512, memory bandwidth becomes the limiting factor. {#tbl-resolution-bottleneck} #### Resolution Strategies in Production {#sec-model-serving-deploymentspecific-resolution-decisions-1d76} -Different deployment contexts impose distinct resolution requirements shaped by their dominant constraints. Mobile applications often accept lower resolution (224$\times$224) for object detection in camera viewfinders, where latency and battery life outweigh marginal accuracy gains. Medical imaging sits at the opposite extreme, requiring 512$\times$512 or higher for diagnostic accuracy, with relaxed latency requirements that permit the additional compute. Autonomous vehicles split the difference by using multiple resolutions for different tasks: low resolution for rapid detection across wide fields of view and high-resolution crops for fine-grained recognition of detected objects. Cloud APIs face yet another challenge—they typically receive images at whatever resolution the client uploads and must handle the resulting range gracefully. This variability makes cloud APIs ideal candidates for adaptive resolution strategies, where the system selects resolution dynamically based on content characteristics. +Different deployment contexts impose distinct resolution requirements shaped by their dominant constraints. Mobile applications often accept lower resolution (224 $\times$ 224) for object detection in camera viewfinders, where latency and battery life outweigh marginal accuracy gains. Medical imaging sits at the opposite extreme, requiring 512 $\times$ 512 or higher for diagnostic accuracy, with relaxed latency requirements that permit the additional compute. Autonomous vehicles split the difference by using multiple resolutions for different tasks: low resolution for rapid detection across wide fields of view and high-resolution crops for fine-grained recognition of detected objects. Cloud APIs face yet another challenge—they typically receive images at whatever resolution the client uploads and must handle the resulting range gracefully. This variability makes cloud APIs ideal candidates for adaptive resolution strategies, where the system selects resolution dynamically based on content characteristics. ```{python} #| label: adaptive-resolution-calc @@ -1268,7 +1269,7 @@ adaptive_accuracy_retention_str = fmt(adaptive_accuracy_retention_value, precisi #### Adaptive Resolution {#sec-model-serving-adaptive-resolution-cb4e} -Production systems\index{Adaptive Resolution!content-based selection} can select resolution dynamically based on content. One approach runs a lightweight classifier at 128$\times$128 to categorize content type, then selects task-appropriate resolution with documents at 512$\times$512, landscapes at 224$\times$224, and faces at 384$\times$384. This achieves `{python} adaptive_throughput_improvement_str`$\times$ throughput improvement with `{python} adaptive_accuracy_retention_str` percent accuracy retention versus fixed high resolution. This pattern trades preprocessing cost from running the lightweight classifier for inference savings on the main model. +Production systems\index{Adaptive Resolution!content-based selection} can select resolution dynamically based on content. One approach runs a lightweight classifier at 128 $\times$ 128 to categorize content type, then selects task-appropriate resolution with documents at 512 $\times$ 512, landscapes at 224 $\times$ 224, and faces at 384 $\times$ 384. This achieves `{python} adaptive_throughput_improvement_str` $\times$ throughput improvement with `{python} adaptive_accuracy_retention_str` percent accuracy retention versus fixed high resolution. This pattern trades preprocessing cost from running the lightweight classifier for inference savings on the main model. The latency analysis so far has focused on sequential processing: one request completing before the next begins. The preprocessing, inference, and postprocessing stages use different hardware resources. This separation creates an opportunity to process multiple requests simultaneously. @@ -1323,7 +1324,7 @@ In the "Quantitative Approach" to ML systems, we define the efficiency of a serv $$\text{System Efficiency} = \frac{\sum T_{\text{compute}}}{\text{Wall Clock Time} \times \text{Resource Count}}$$ {#eq-system-efficiency} -If a ResNet-50 request takes 10 ms total (5 ms GPU, 5 ms CPU), a serial system achieves only 50% efficiency. By pipelining just two requests, efficiency approaches 100% (assuming the CPU can keep up with the GPU). If the CPU is too slow to feed the GPU, the system becomes CPU-bound, and further model optimization provides zero throughput gain—a direct application of Amdahl's Law (introduced in @sec-ml-systems) to serving: if preprocessing consumes 50% of latency, maximum speedup is 2$\times$ regardless of how fast the model runs. +If a ResNet-50 request takes 10 ms total (5 ms GPU, 5 ms CPU), a serial system achieves only 50% efficiency. By pipelining just two requests, efficiency approaches 100% (assuming the CPU can keep up with the GPU). If the CPU is too slow to feed the GPU, the system becomes CPU-bound, and further model optimization provides zero throughput gain—a direct application of Amdahl's Law (introduced in @sec-ml-systems) to serving: if preprocessing consumes 50% of latency, maximum speedup is 2 $\times$ regardless of how fast the model runs. ### Postprocessing {#sec-model-serving-postprocessing-3b24} @@ -1483,17 +1484,17 @@ This equation reveals why serving systems exhibit nonlinear behavior: small incr The M/M/1 model assumes exponentially distributed service times, but ML inference typically has near-constant service time for fixed batch sizes, making the M/D/1\index{M/D/1 Queue!deterministic service} (deterministic service) model more accurate in practice. We use M/M/1 here because it yields closed-form solutions and produces conservative estimates. For M/D/1 queues, average wait time is approximately half of M/M/1 at the same utilization, which matters for capacity planning: M/M/1 analysis will slightly over-provision, erring on the side of meeting SLOs rather than violating them.[^fn-queuing-models] -[^fn-queuing-models]: **Kendall Notation**: The M/M/1 notation was introduced by British statistician David Kendall in 1953 and follows the pattern A/S/c (Arrivals/Service/servers). "M" stands for "Markovian" (memoryless, meaning exponential distributions), honoring Russian mathematician Andrey Markov (1856-1922). "D" means deterministic. So M/M/1 describes a single server with exponential arrivals and service times, while M/D/1 has deterministic service. ML inference is closer to M/D/1 since inference time is nearly constant, but M/M/1 yields conservative estimates suitable for capacity planning. The mathematics underlying all queuing models traces to Agner Krarup Erlang, a Danish engineer at the Copenhagen Telephone Company who published the foundational formulas in 1909 to determine how many telephone circuits were needed to handle call traffic—the same capacity planning problem ML engineers face today with inference requests instead of phone calls. +[^fn-queuing-models]: **Kendall Notation**: The M/M/1 notation was introduced by British statistician David Kendall [@kendall1953stochastic] and follows the pattern A/S/c (Arrivals/Service/servers). "M" stands for "Markovian" (memoryless, meaning exponential distributions), honoring Russian mathematician Andrey Markov (1856-1922). "D" means deterministic. So M/M/1 describes a single server with exponential arrivals and service times, while M/D/1 has deterministic service. ML inference is closer to M/D/1 since inference time is nearly constant, but M/M/1 yields conservative estimates suitable for capacity planning. The mathematics underlying all queuing models traces to Agner Krarup Erlang, a Danish engineer at the Copenhagen Telephone Company who published the foundational formulas in 1909 to determine how many telephone circuits were needed to handle call traffic—the same capacity planning problem ML engineers face today with inference requests instead of phone calls. | **Utilization ($\rho$)** | **Latency Multiple** | **Example (5ms service)** | |:-------------------------|---------------------:|--------------------------:| -| 50% | 2.0$\times$ | 10ms | -| 70% | 3.3$\times$ | 17ms | -| 80% | 5.0$\times$ | 25ms | -| 90% | 10.0$\times$ | 50ms | -| 95% | 20.0$\times$ | 100ms | +| 50% | 2.0 $\times$ | 10ms | +| 70% | 3.3 $\times$ | 17ms | +| 80% | 5.0 $\times$ | 25ms | +| 90% | 10.0 $\times$ | 50ms | +| 95% | 20.0 $\times$ | 100ms | -: **Utilization-Latency Relationship**: Average **time in system** (wait + service) as a multiple of service time for an M/M/1 queue. At 50% utilization, time in system is 2$\times$ service time; at 90%, it reaches 10$\times$. This nonlinear growth explains why systems that perform well at moderate load suddenly violate SLOs when traffic increases: moving from 80% to 90% utilization doubles latency. {#tbl-utilization-latency} +: **Utilization-Latency Relationship**: Average **time in system** (wait + service) as a multiple of service time for an M/M/1 queue. At 50% utilization, time in system is 2 $\times$ service time; at 90%, it reaches 10 $\times$. This nonlinear growth explains why systems that perform well at moderate load suddenly violate SLOs when traffic increases: moving from 80% to 90% utilization doubles latency. {#tbl-utilization-latency} ### Multi-Server Considerations {#sec-model-serving-multiserver-considerations-00fc} @@ -1507,7 +1508,7 @@ M/M/1 analysis remains the foundation for: - **Identifying the scaling trigger**: Calculating when traffic exceeds single-node capacity - **Cost-effective provisioning**: Avoiding premature scale-out that wastes resources -For traffic exceeding single-node capacity, production systems deploy multiple replicas behind a load balancer. The M/M/c queuing model\index{M/M/c Queue!multi-server} extends M/M/1 to c parallel servers, showing that multiple replicas\index{Replica!tail latency improvement} dramatically improve tail latency: the probability of all servers being simultaneously slow drops exponentially with server count. At c=4 replicas and moderate utilization, p99 latency can be 3$\times$ lower than the single-server case at the same total throughput. This chapter establishes single-node serving foundations; distributed inference systems (model sharding across GPUs, tensor parallelism, pipeline parallelism) introduce coordination overhead and consistency challenges that require advanced scaling principles beyond our scope here. +For traffic exceeding single-node capacity, production systems deploy multiple replicas behind a load balancer. The M/M/c queuing model\index{M/M/c Queue!multi-server} extends M/M/1 to c parallel servers, showing that multiple replicas\index{Replica!tail latency improvement} dramatically improve tail latency: the probability of all servers being simultaneously slow drops exponentially with server count. At c=4 replicas and moderate utilization, p99 latency can be 3 $\times$ lower than the single-server case at the same total throughput. This chapter establishes single-node serving foundations; distributed inference systems (model sharding across GPUs, tensor parallelism, pipeline parallelism) introduce coordination overhead and consistency challenges that require advanced scaling principles beyond our scope here. ### Tail Latency {#sec-model-serving-tail-latency-5376} @@ -1644,7 +1645,7 @@ Eliminating all sources of latency variability is often impractical. Production When\index{Hedged Requests!tail tolerance} a request has not completed within the expected time, the system sends a duplicate request to another server.[^fn-hedging-etymology] The client uses whichever response arrives first and cancels the other. For ML serving, this means maintaining multiple model replicas and routing slow requests to alternative replicas. The overhead is modest: if you hedge at the 95th percentile, only 5% of requests generate duplicates, increasing load by just 5% while dramatically reducing tail latency. -[^fn-hedging-etymology]: **Hedging**: Borrowed from finance, where "hedging" means reducing risk by making offsetting bets. The term derives from the literal hedge (a boundary of shrubs) that protects a garden. Financial hedging dates to the 1600s Dutch tulip markets. Google's Jeff Dean introduced "hedged requests" in his influential 2013 "Tail at Scale" paper, applying the financial concept to distributed systems: send redundant requests to protect against the risk of slow responses. +[^fn-hedging-etymology]: **Hedging**: Borrowed from finance, where "hedging" means reducing risk by making offsetting bets. The term derives from the literal hedge (a boundary of shrubs) that protects a garden. Financial hedging dates to the 1600s Dutch tulip markets. Google's Jeff Dean introduced "hedged requests" in his influential "Tail at Scale" paper [@dean2013tail], applying the financial concept to distributed systems: send redundant requests to protect against the risk of slow responses. CUDA kernels cannot be interrupted mid-execution. When a hedged request completes, the duplicate must be cancelled, but if inference has already begun on the GPU, cancellation approaches include checking a cancellation flag before launching inference, accepting wasted compute for the in-flight kernel, or using request prioritization to deprioritize the duplicate. Since hedging typically applies only to the slowest 5 percent of requests, the overhead from occasional wasted compute remains acceptable. @@ -1657,6 +1658,7 @@ Tied requests\index{Tied Requests!latency reduction} send the request to multipl For\index{Canary Requests!fan-out protection} requests that fan out to many backends, first send the request to a small subset of 1 to 2 servers.[^fn-canary-etymology] If these return within expected time, send to the remainder. If the canary is slow, the system can take corrective action by retrying elsewhere or using cached results before committing to the full fan-out. This prevents a single slow backend from stalling an entire distributed inference request. \index{Canary!etymology} + [^fn-canary-etymology]: **Canary**: From the practice of using canary birds in coal mines from the early 1900s through the 1980s. Miners brought caged canaries underground because the birds' high metabolic rate made them sensitive to carbon monoxide and methane, dying before gas concentrations became lethal to humans. In software, "canary" describes any small-scale test that detects problems before they affect the full system, whether canary deployments, canary requests, or canary tests. #### Graceful Degradation {#sec-model-serving-graceful-degradation-d1d8} @@ -1865,13 +1867,13 @@ Tload = `{python} model_size_gb_str` GB / `{python} pcie_bw_gbs_str` GB/s ≈ `{ To mitigate this, systems use *pinned memory*\index{Pinned Memory!DMA transfer} (page-locked host memory). By default, the operating system can move ("page") any memory region to disk when RAM is under pressure. This creates a problem for GPU transfers: if the GPU's DMA (Direct Memory Access) engine begins reading a memory region that gets paged out mid-transfer, the transfer fails or stalls. To avoid this, the CPU must first copy data to a temporary pinned buffer before the GPU can safely read it, adding both latency and CPU overhead. -Pinning memory instructs the OS to keep that region permanently in physical RAM. The GPU's DMA engine can then transfer data directly from the pinned region at full PCIe bandwidth without CPU involvement. The trade-off is that pinned memory reduces the RAM available for other processes and cannot be reclaimed under memory pressure. For model serving, the performance gain (2–3$\times$ faster transfers) typically justifies pinning model weights and frequently-used input buffers, while leaving less critical memory pageable. +Pinning memory instructs the OS to keep that region permanently in physical RAM. The GPU's DMA engine can then transfer data directly from the pinned region at full PCIe bandwidth without CPU involvement. The trade-off is that pinned memory reduces the RAM available for other processes and cannot be reclaimed under memory pressure. For model serving, the performance gain (2–3 $\times$ faster transfers) typically justifies pinning model weights and frequently-used input buffers, while leaving less critical memory pageable. The lifecycle management strategies examined so far ensure models are ready to serve: loaded into memory, warmed up, and producing predictions consistent with training. With these prerequisites satisfied, the queuing dynamics from @sec-model-serving-queuing-theory-tail-latency-29a6 become relevant. The next optimization opportunity lies in how requests are grouped for processing, which directly affects both the throughput and latency terms in our queuing equations. ## Throughput Optimization {#sec-model-serving-throughput-optimization-18d1} -Consider a ResNet-50 classifier running on a V100 GPU at batch size 1: the GPU processes one image, then sits idle while the CPU fetches and preprocesses the next—achieving only 15% hardware utilization and 200 images per second. The same GPU processing 32 images at once reaches 95% utilization and 1,280 images per second, a 6.4$\times$ throughput improvement on identical hardware. The difference is batching, the core lever for improving serving economics. Batching\index{Batching!training vs serving}\index{Batching!throughput optimization}[^fn-batch-etymology] differs sharply between training and serving [@crankshaw2017clipper]. Training batches maximize throughput by processing hundreds or thousands of samples together with no concern for individual sample latency. Serving batches must balance throughput against individual request latency, typically processing single digits of requests together while ensuring no request waits too long. This adaptive approach is called **dynamic batching** because the system adjusts batch composition in real time based on arriving requests. +Consider a ResNet-50 classifier running on a V100 GPU at batch size 1: the GPU processes one image, then sits idle while the CPU fetches and preprocesses the next—achieving only 15% hardware utilization and 200 images per second. The same GPU processing 32 images at once reaches 95% utilization and 1,280 images per second, a 6.4 $\times$ throughput improvement on identical hardware. The difference is batching, the core lever for improving serving economics. Batching\index{Batching!training vs serving}\index{Batching!throughput optimization}[^fn-batch-etymology] differs sharply between training and serving [@crankshaw2017clipper]. Training batches maximize throughput by processing hundreds or thousands of samples together with no concern for individual sample latency. Serving batches must balance throughput against individual request latency, typically processing single digits of requests together while ensuring no request waits too long. This adaptive approach is called **dynamic batching** because the system adjusts batch composition in real time based on arriving requests. [^fn-batch-etymology]: **Batch**: From Old French "bache" (a quantity baked at one time), the term entered computing in the 1950s to describe jobs processed together without human interaction, as contrasted with interactive computing. IBM's batch processing systems of the 1960s would collect punch cards overnight and process them sequentially. The ML usage preserves this core meaning: group samples together for efficient processing, trading individual response time for aggregate throughput. @@ -1885,7 +1887,7 @@ Consider a ResNet-50 classifier running on a V100 GPU at batch size 1: the GPU p Modern accelerators achieve peak efficiency only at sufficient batch sizes\index{GPU Utilization!batch size dependency} [@shen2019nexus]. A single inference request leaves most compute units idle because GPUs are designed for parallel execution across thousands of threads. Batching amortizes fixed costs across multiple requests and enables parallel execution across the batch dimension. -Two fixed costs dominate at small batch sizes. **Kernel launch overhead**\index{Kernel Launch Overhead!fixed cost}[^fn-kernel-etymology-serving] is the time for the CPU to prepare and submit work to the GPU. Each layer in a neural network typically requires a separate kernel launch: the CPU must assemble kernel parameters, copy them to GPU-accessible memory, and signal the GPU to begin execution. This overhead is typically 5–20 μs per kernel, independent of batch size. ResNet-50 has approximately 50 layers, so kernel launch alone adds 250–1000 μs per inference. At batch size 1, this overhead may exceed the actual compute time; at batch size 32, the same overhead is amortized across 32 images. **Weight loading**\index{Weight Loading!memory efficiency} reads model parameters from GPU memory (VRAM) to the compute units. At batch size 1, the GPU reads all weights to process one image; at batch size 32, the same weight read processes 32 images, achieving 32$\times$ better memory efficiency. Measuring *batching efficiency* on a concrete model quantifies how these fixed costs amortize in practice. +Two fixed costs dominate at small batch sizes. **Kernel launch overhead**\index{Kernel Launch Overhead!fixed cost}[^fn-kernel-etymology-serving] is the time for the CPU to prepare and submit work to the GPU. Each layer in a neural network typically requires a separate kernel launch: the CPU must assemble kernel parameters, copy them to GPU-accessible memory, and signal the GPU to begin execution. This overhead is typically 5–20 μs per kernel, independent of batch size. ResNet-50 has approximately 50 layers, so kernel launch alone adds 250–1000 μs per inference. At batch size 1, this overhead may exceed the actual compute time; at batch size 32, the same overhead is amortized across 32 images. **Weight loading**\index{Weight Loading!memory efficiency} reads model parameters from GPU memory (VRAM) to the compute units. At batch size 1, the GPU reads all weights to process one image; at batch size 32, the same weight read processes 32 images, achieving 32 $\times$ better memory efficiency. Measuring *batching efficiency* on a concrete model quantifies how these fixed costs amortize in practice. [^fn-kernel-etymology-serving]: **Kernel**: From Old English "cyrnel" meaning seed or grain, the essential core of something. In operating systems (1960s), the kernel is the core that manages hardware resources. CUDA borrowed this term around 2007 for GPU functions because they represent the computational "core" of parallel algorithms. Unlike OS kernels that run continuously, GPU kernels are discrete units of parallel work launched by the CPU and executed across thousands of GPU threads simultaneously. @@ -1940,7 +1942,7 @@ The throughput-latency tradeoff for ResNet-50 on a V100 GPU illustrates the powe Note: Times shown are pure inference time, excluding queue wait. @sec-model-serving-traffic-patterns-batching-strategy-2e6b analyzes how user-perceived latency includes batching window wait. -**Key insight**: Batch size 32 achieves `{python} throughput_ratio_str`$\times$ higher throughput than batch size 1. However, user-perceived latency includes both queue wait and inference time. With a `{python} batch_window_ms_str` ms batching window and `{python} batch32_inference_ms_str` ms inference, total latency reaches `{python} batch32_total_str` ms versus `{python} batch1_inference_total_ms_str` ms at batch size 1. +**Key insight**: Batch size 32 achieves `{python} throughput_ratio_str` $\times$ higher throughput than batch size 1. However, user-perceived latency includes both queue wait and inference time. With a `{python} batch_window_ms_str` ms batching window and `{python} batch32_inference_ms_str` ms inference, total latency reaches `{python} batch32_total_str` ms versus `{python} batch1_inference_total_ms_str` ms at batch size 1. ::: @@ -1997,7 +1999,7 @@ latency_increase_str = fmt(latency_increase_value, precision=0, commas=False) - **User Latency**: `{python} batch8_wait_ms_str` ms (wait) + `{python} batch8_inference_ms_str` ms (compute) = **`{python} batch8_user_latency_str` ms**. - **Throughput**: 8 img / `{python} batch8_user_latency_str` ms ≈ **`{python} batch8_throughput_str` img/s**. -**The Systems Conclusion**: By accepting a **`{python} latency_increase_str`$\times$ increase in latency** (`{python} batch1_ms_str` ms → `{python} batch8_user_latency_str` ms), you have achieved nearly **`{python} latency_increase_str`$\times$ higher throughput** on the same hardware. As long as `{python} batch8_user_latency_str` ms is under your 20ms budget, this is "free" capacity. This trade-off is the primary lever of serving economics. +**The Systems Conclusion**: By accepting a **`{python} latency_increase_str` $\times$ increase in latency** (`{python} batch1_ms_str` ms → `{python} batch8_user_latency_str` ms), you have achieved nearly **`{python} latency_increase_str` $\times$ higher throughput** on the same hardware. As long as `{python} batch8_user_latency_str` ms is under your 20ms budget, this is "free" capacity. This trade-off is the primary lever of serving economics. ::: Look for the **"Knee"** in @fig-throughput-latency-knee, the point where the blue throughput curve begins to plateau just as the orange latency curve starts its sharp upward spike. This is the optimal operating point: push batch size beyond the knee and queuing delays dominate; stay below it and you leave hardware capacity on the table. The numbers are representative rather than tied to a single benchmark. @@ -2125,12 +2127,12 @@ $$P(\text{batch size} = k) = \frac{(\lambda T)^k e^{-\lambda T}}{k!}$$ {#eq-batc @tbl-batch-variability quantifies this variability, showing how batch size fluctuates for different traffic levels with a fixed 10 ms window: -| **Arrival Rate** | **Mean Batch** | **Std Dev** | **P(batch=0)** | **P(batch≥2$\times$mean)** | -|:-----------------|---------------:|------------:|---------------:|---------------------------:| -| **50 QPS** | 0.5 | 0.7 | 61% | 39% | -| **200 QPS** | 2.0 | 1.4 | 14% | 14% | -| **500 QPS** | 5.0 | 2.2 | 0.7% | 3% | -| **1000 QPS** | 10.0 | 3.2 | 0.005% | 0.3% | +| **Arrival Rate** | **Mean Batch** | **Std Dev** | **P(batch=0)** | **P(batch≥2 $\times$ mean)** | +|:-----------------|---------------:|------------:|---------------:|-----------------------------:| +| **50 QPS** | 0.5 | 0.7 | 61% | 39% | +| **200 QPS** | 2.0 | 1.4 | 14% | 14% | +| **500 QPS** | 5.0 | 2.2 | 0.7% | 3% | +| **1000 QPS** | 10.0 | 3.2 | 0.005% | 0.3% | : **Batch Size Variability**: At low traffic, batching windows frequently contain zero requests (wasted GPU cycles). At moderate traffic, batch sizes fluctuate significantly around the mean. High traffic provides more stable batching, and the probability of batches exceeding twice the mean size decreases as traffic grows (from 39% at 50 QPS to 0.3% at 1000 QPS), reflecting the law of large numbers. {#tbl-batch-variability} @@ -2210,7 +2212,7 @@ T_window_str = fmt(T_window_value, precision=0, commas=False) | 16 | 14.6ms | 24.6ms | 650 img/s | High | | 32 | 24.2ms | 34.2ms | 935 img/s | Maximum | -: **Batching Throughput Analysis**: ResNet-50 throughput on V100 with 10 ms batching window. Throughput increases 14.6$\times$ from batch size 1 to 32 (64 to 935 img/s), but total latency more than doubles (15.6 ms to 34.2 ms). The optimal configuration depends on whether the latency SLO or throughput target is the binding constraint. {#tbl-batching-throughput} +: **Batching Throughput Analysis**: ResNet-50 throughput on V100 with 10 ms batching window. Throughput increases 14.6 $\times$ from batch size 1 to 32 (64 to 935 img/s), but total latency more than doubles (15.6 ms to 34.2 ms). The optimal configuration depends on whether the latency SLO or throughput target is the binding constraint. {#tbl-batching-throughput} The throughput gains in @tbl-batching-throughput trace directly back to *the Iron Law of batching efficiency*, the framework established in @sec-model-training-iron-law-training-performance-a53f, where batching amortizes the fixed overhead term. @@ -2410,7 +2412,7 @@ where bp99 is the 99th percentile batch size. For lambda = `{python} qps_str` QP - Mean latency: `{python} mean_wait_str` ms + `{python} mean_service_str` ms = `{python} mean_latency_str` ms - p99 latency: `{python} T_slo_str` ms + `{python} p99_service_str` ms = `{python} p99_latency_str` ms -The p99 latency is `{python} p99_ratio_str`$\times$ the mean, reflecting both wait time variance and batch size variance. Systems that provision based on mean latency will experience SLO violations. +The p99 latency is `{python} p99_ratio_str` $\times$ the mean, reflecting both wait time variance and batch size variance. Systems that provision based on mean latency will experience SLO violations. ::: {.callout-perspective title="Practitioner's Perspective: The Latency-Throughput Trade-off" collapse="false"} In systems engineering interviews and architecture reviews, the most common pitfall is discussing "inference speed" without specifying batch size. @@ -2473,13 +2475,13 @@ The batching configuration space forms a Pareto frontier[^fn-pareto-frontier-bat | 20 | 64 | 23ms | 52ms | 1,310 img/s | Throughput-optimized | | 50 | 128 | 38ms | 98ms | 1,350 img/s | Maximum throughput | -: **Batching Pareto Frontier**: Each configuration represents a different point on the throughput-latency trade-off curve. Moving from 2ms to 50ms windows improves throughput by only 52% while increasing p99 latency by 5.4$\times$. Diminishing returns make aggressive batching costly for latency-sensitive applications. {#tbl-pareto-batching} +: **Batching Pareto Frontier**: Each configuration represents a different point on the throughput-latency trade-off curve. Moving from 2ms to 50ms windows improves throughput by only 52% while increasing p99 latency by 5.4 $\times$. Diminishing returns make aggressive batching costly for latency-sensitive applications. {#tbl-pareto-batching} #### Practical Configuration Guidelines {#sec-model-serving-practical-configuration-guidelines-9791} -[^fn-pareto-frontier-batching]: **Pareto Frontier**: Named after Italian economist Vilfredo Pareto (1848--1923), who observed in 1896 that 80% of Italy's land was owned by 20% of the population. The "Pareto frontier" (also called "Pareto optimal" or "efficient frontier") describes the set of solutions where no objective can be improved without worsening another. In serving systems, the frontier maps the throughput-latency trade-off: each point represents a configuration where gaining throughput requires sacrificing latency, and vice versa. The concept originated in welfare economics but pervades engineering optimization, from portfolio theory (Markowitz, 1952) to multi-objective neural architecture search. +[^fn-pareto-frontier-batching]: **Pareto Frontier**: Named after Italian economist Vilfredo Pareto (1848--1923), who observed in 1896 that 80% of Italy's land was owned by 20% of the population. The "Pareto frontier" (also called "Pareto optimal" or "efficient frontier") describes the set of solutions where no objective can be improved without worsening another. In serving systems, the frontier maps the throughput-latency trade-off: each point represents a configuration where gaining throughput requires sacrificing latency, and vice versa. The concept originated in welfare economics but pervades engineering optimization, from portfolio theory [@markowitz1952portfolio] to multi-objective neural architecture search. -The Pareto frontier in @tbl-pareto-batching illustrates why these guidelines matter: moving from a 2ms to a 50ms window improves throughput by only 52% while increasing p99 latency by 5.4$\times$. Principled batching configuration avoids this region of diminishing returns by working backward from the latency budget. Allocating 20 to 30 percent of the SLO to batching wait time leaves the remainder for inference and overhead, which bounds the maximum window at $T_{\text{max}} = 0.3 \times L_{\text{lat,SLO}}$. The traffic estimate that feeds this calculation should use the p95 arrival rate rather than the average, because batching windows tuned for average traffic produce oversized batches during spikes—precisely when SLO headroom matters most. GPU memory imposes a hard ceiling on batch size independent of the latency constraint, since activation memory scales linearly with the batch dimension. Finally, monitoring the actual batch size distribution in production reveals whether initial traffic assumptions hold; high variance signals that the window needs adaptive tuning rather than a fixed configuration. +The Pareto frontier in @tbl-pareto-batching illustrates why these guidelines matter: moving from a 2ms to a 50ms window improves throughput by only 52% while increasing p99 latency by 5.4 $\times$. Principled batching configuration avoids this region of diminishing returns by working backward from the latency budget. Allocating 20 to 30 percent of the SLO to batching wait time leaves the remainder for inference and overhead, which bounds the maximum window at $T_{\text{max}} = 0.3 \times L_{\text{lat,SLO}}$. The traffic estimate that feeds this calculation should use the p95 arrival rate rather than the average, because batching windows tuned for average traffic produce oversized batches during spikes—precisely when SLO headroom matters most. GPU memory imposes a hard ceiling on batch size independent of the latency constraint, since activation memory scales linearly with the batch dimension. Finally, monitoring the actual batch size distribution in production reveals whether initial traffic assumptions hold; high variance signals that the window needs adaptive tuning rather than a fixed configuration. For ResNet-50 with 50ms SLO and 500 QPS traffic: @@ -2554,9 +2556,9 @@ The mechanism works as follows: when a sequence generates its end-of-sequence to This dynamic approach maintains high GPU utilization even when sequence lengths vary dramatically. -Systems implementing continuous batching, such as vLLM[^fn-vllm] and TensorRT-LLM[^fn-tensorrt-llm], achieve 2–4$\times$ higher throughput than traditional static batching [@agrawal2024sarathi]. The improvement comes from two sources: eliminating wasted compute on completed sequences and reducing average wait time for new requests. For production language model serving where response lengths vary from single tokens to thousands, continuous batching has become essential for cost-effective deployment. +Systems implementing continuous batching, such as vLLM[^fn-vllm] and TensorRT-LLM[^fn-tensorrt-llm], achieve 2–4 $\times$ higher throughput than traditional static batching [@agrawal2024sarathi]. The improvement comes from two sources: eliminating wasted compute on completed sequences and reducing average wait time for new requests. For production language model serving where response lengths vary from single tokens to thousands, continuous batching has become essential for cost-effective deployment. -[^fn-vllm]: **vLLM**: Open-sourced by UC Berkeley researchers Woosuk Kwon, Zhuohan Li, and others in June 2023 alongside their SOSP paper introducing PagedAttention. The name stands for "virtual LLM," drawing a deliberate analogy to virtual memory in operating systems: just as virtual memory decouples logical addresses from physical RAM pages, vLLM decouples the logical KV cache from contiguous GPU memory blocks. This OS-inspired design solved the memory fragmentation problem that limited prior LLM serving systems to 50--60% memory utilization, enabling 2--4$\times$ higher throughput at the same hardware cost. +[^fn-vllm]: **vLLM**: Open-sourced by UC Berkeley researchers Woosuk Kwon, Zhuohan Li, and others in June 2023 alongside their SOSP paper introducing PagedAttention. The name stands for "virtual LLM," drawing a deliberate analogy to virtual memory in operating systems: just as virtual memory decouples logical addresses from physical RAM pages, vLLM decouples the logical KV cache from contiguous GPU memory blocks. This OS-inspired design solved the memory fragmentation problem that limited prior LLM serving systems to 50--60% memory utilization, enabling 2--4 $\times$ higher throughput at the same hardware cost. [^fn-tensorrt-llm]: **TensorRT-LLM**: NVIDIA's open-source library (released October 2023) that extends TensorRT's inference optimization to large language models. Built on top of TensorRT's graph compilation and kernel fusion capabilities, it adds LLM-specific optimizations: in-flight batching (NVIDIA's term for continuous batching), paged KV cache management, multi-GPU tensor parallelism, and custom attention kernels. The library provides a Python API for defining models that compile to optimized TensorRT engines, bridging the gap between research model code and production-grade serving performance. @@ -2566,13 +2568,13 @@ Memory management adds complexity to continuous batching. As sequences enter and PagedAttention\index{Continuous Batching!iteration-level}\index{PagedAttention!memory fragmentation solution},[^fn-pagedattention] introduced in vLLM, solves this fragmentation problem by applying operating system virtual memory concepts to GPU memory [@kwon2023vllm]. Instead of allocating one contiguous block per sequence, PagedAttention divides the KV cache into fixed-size *pages* (typically 16 tokens each). A sequence's cache consists of pointers to non-contiguous pages scattered across GPU memory. When a sequence completes, its pages return to a free list and can be reused by any new sequence, regardless of length. This approach achieves near-zero fragmentation: vLLM reports memory utilization above 95% compared to 50–60% for contiguous allocation schemes. The overhead is modest (one pointer lookup per page during attention computation), making PagedAttention the standard for production LLM serving. -[^fn-pagedattention]: **PagedAttention**: Introduced by Kwon et al. at SOSP 2023, this algorithm directly applies operating system virtual memory concepts to GPU memory management for LLMs. Before PagedAttention, researchers found that existing systems wasted 60–80% of KV cache memory due to fragmentation and over-reservation. By borrowing paging and copy-on-write mechanisms from OS design, PagedAttention reduces waste to under 4%, enabling 2–4$\times$ higher throughput on the same hardware. This technique has become the de facto standard in production LLM serving systems. +[^fn-pagedattention]: **PagedAttention**: Introduced by Kwon et al. at SOSP 2023, this algorithm directly applies operating system virtual memory concepts to GPU memory management for LLMs. Before PagedAttention, researchers found that existing systems wasted 60–80% of KV cache memory due to fragmentation and over-reservation. By borrowing paging and copy-on-write mechanisms from OS design, PagedAttention reduces waste to under 4%, enabling 2–4 $\times$ higher throughput on the same hardware. This technique has become the de facto standard in production LLM serving systems. The batching and memory techniques covered here establish the foundation for LLM serving, but several advanced topics warrant additional study: ::: {.callout-perspective title="LLM Serving: Beyond the Fundamentals"} -Language model serving introduces challenges beyond the batching and memory principles established here. The key-value cache that stores attention context scales with sequence length and batch size, often exceeding the model weights themselves in memory consumption. Techniques like speculative decoding\index{Speculative Decoding!latency reduction}\index{Speculative Decoding!draft model verification} use small draft models to propose multiple tokens that the target model verifies in parallel, achieving 2–3$\times$ latency reduction for interactive applications. Weight-only quantization (INT4 weights with FP16 activations) proves more effective than activation quantization for memory-bandwidth-bound LLM inference. +Language model serving introduces challenges beyond the batching and memory principles established here. The key-value cache that stores attention context scales with sequence length and batch size, often exceeding the model weights themselves in memory consumption. Techniques like speculative decoding\index{Speculative Decoding!latency reduction}\index{Speculative Decoding!draft model verification} use small draft models to propose multiple tokens that the target model verifies in parallel, achieving 2–3 $\times$ latency reduction for interactive applications. Weight-only quantization (INT4 weights with FP16 activations) proves more effective than activation quantization for memory-bandwidth-bound LLM inference. These LLM-specific optimizations build directly on the foundations this chapter establishes: queuing theory governs request scheduling, batching tradeoffs determine throughput-latency curves, and precision selection follows the same accuracy-efficiency principles. The serving fundamentals apply universally; LLM serving adds domain-specific techniques atop this foundation. Advanced treatments provide detailed coverage of KV cache optimization, including advanced techniques for multi-tenant serving and distributed inference. @@ -2731,7 +2733,7 @@ m_total_mj_str = f"{m_total_mj_value:.1f}mJ" - **Energy per inference**: 3.0mJ enables ~12 million inferences per 10Wh battery (typical smartphone) - **Thermal budget**: At 3.0mJ/45ms = 67mW sustained, indefinite operation without throttling -- **NPU vs CPU tradeoff**: CPU fallback uses 4.2mJ (1.4$\times$ energy) at 85ms (1.9$\times$ latency) +- **NPU vs CPU tradeoff**: CPU fallback uses 4.2mJ (1.4 $\times$ energy) at 85ms (1.9 $\times$ latency) - **Memory footprint**: 150MB peak (model + activations), competing with app memory **Critical insight**: Even at batch size 1, the mobile NPU achieves 82% utilization because its compute capacity matches single-image workloads. This differs from datacenter GPUs, which achieve only 15% utilization at batch size 1 because their massive parallelism requires larger batches to saturate. @@ -2764,7 +2766,7 @@ These constraints make mobile serving optimization qualitatively different from :::: {.callout-checkpoint title="Batching and Traffic Patterns" collapse="false"} Batching is the primary lever for serving economics, but the optimal strategy depends on context. -- [ ] **Throughput-latency tradeoff**: Can you explain why batch size 32 achieves 6$\times$ higher throughput than batch size 1, yet a production system with a 20ms SLO might still choose batch size 8? +- [ ] **Throughput-latency tradeoff**: Can you explain why batch size 32 achieves 6 $\times$ higher throughput than batch size 1, yet a production system with a 20ms SLO might still choose batch size 8? - [ ] **Dynamic vs. static batching**: Can you describe why static batching (waiting for a full batch) fails under variable traffic, and how dynamic batching with a time window solves this? - [ ] **Traffic pattern matching**: Given a deployment scenario (e.g., cloud API, autonomous vehicle, mobile app), can you select the appropriate MLPerf scenario and explain why that batching strategy fits? - [ ] **Adaptive windows**: Can you explain why the optimal batching window *decreases* as traffic *increases*, even though batch sizes grow? @@ -2808,11 +2810,11 @@ Generative models require decoding strategies that trade off quality, diversity, The simplest approach, greedy decoding\index{Greedy Decoding!LLM generation}, selects the highest-probability token at each step. It is fast but often produces repetitive, low-quality outputs because it cannot recover from early mistakes. Beam search[^fn-beam-search]\index{Beam Search!decoding strategy}\index{Beam Search!candidate sequences} improves quality by maintaining multiple candidate sequences and selecting the highest-scoring complete sequence, though it multiplies computation by the beam width. Sampling\index{Sampling!temperature, top-k, top-p} with temperature, top-k, and top-p parameters introduces randomness for diversity [@holtzman2020curious]. Temperature scales logits before softmax. Top-k limits sampling to the k highest-probability tokens. Top-p, also called nucleus sampling[^fn-nucleus-sampling]\index{Nucleus Sampling!top-p}, limits sampling to tokens comprising probability mass p. -[^fn-beam-search]: **Beam Search**: A heuristic search algorithm that explores a graph by expanding only the most promising nodes at each level. The "beam" metaphor evokes a flashlight illuminating a narrow band of the search space—wider beams (larger beam widths) explore more possibilities but cost proportionally more compute and memory. Beam search originated in speech recognition in the 1970s (Raj Reddy's group at CMU) and was adopted for neural machine translation by Sutskever et al. (2014). For LLM serving, beam width directly multiplies memory requirements because each beam maintains its own KV cache, making beam width a critical serving cost parameter. +[^fn-beam-search]: **Beam Search**: A heuristic search algorithm that explores a graph by expanding only the most promising nodes at each level. The "beam" metaphor evokes a flashlight illuminating a narrow band of the search space—wider beams (larger beam widths) explore more possibilities but cost proportionally more compute and memory. Beam search originated in speech recognition in the 1970s (Raj Reddy's group at CMU) and was adopted for neural machine translation by Sutskever et al. [@sutskever2014sequence]. For LLM serving, beam width directly multiplies memory requirements because each beam maintains its own KV cache, making beam width a critical serving cost parameter. [^fn-nucleus-sampling]: **Nucleus Sampling (Top-p)**: Introduced by Ari Holtzman et al. in "The Curious Case of Neural Text Degeneration" [@holtzman2020curious] (2019). The "nucleus" metaphor comes from the core of the probability distribution: rather than sampling from a fixed number of top tokens (top-k), nucleus sampling dynamically selects the smallest set of tokens whose cumulative probability exceeds a threshold *p* (typically 0.9--0.95). This adapts to the shape of the distribution: when the model is confident, few tokens are sampled; when uncertain, more diversity is allowed. The approach elegantly solves the problem that top-k with fixed *k* is either too restrictive (cutting off valid tokens) or too permissive (including nonsensical ones) depending on context. -The choice presents latency tradeoffs [@meister2020beam]. Beam search with width 5 takes roughly 5$\times$ the compute of greedy decoding. Sampling adds minimal overhead but requires careful parameter tuning to balance quality and coherence. +The choice presents latency tradeoffs [@meister2020beam]. Beam search with width 5 takes roughly 5 $\times$ the compute of greedy decoding. Sampling adds minimal overhead but requires careful parameter tuning to balance quality and coherence. Production LLM systems\index{Streaming Responses!LLM serving}\index{Chunked HTTP!streaming tokens} return tokens as they are produced rather than waiting for complete generation. This transforms the user experience: a 2-second total generation feels responsive when tokens stream continuously, but feels broken when users stare at a blank screen for 2 seconds. Streaming requires infrastructure support for chunked HTTP responses and client-side incremental rendering. The latency profile shifts accordingly: TTFT determines when output starts appearing (responsiveness), while TPOT determines the perceived generation speed (fluidity). @@ -2820,7 +2822,7 @@ Production LLM systems\index{Streaming Responses!LLM serving}\index{Chunked HTTP Generative inference requires managing the **KV Cache**[^fn-kv-cache]\index{KV Cache!LLM memory}\index{KV Cache!sequence length scaling}, a stateful memory structure that grows with sequence length. Unlike traditional models where memory usage is constant per batch, LLM memory usage is dynamic. Each generated token adds to the context window, consuming additional GPU memory through state accumulation, and variable-length sequences can lead to memory fragmentation if not managed explicitly. -[^fn-kv-cache]: **KV Cache (Key-Value Cache)**: In transformer attention (introduced by Vaswani et al. in 2017), each layer computes Key and Value projections from input tokens. During autoregressive generation, previously computed K and V vectors remain valid for all future tokens—only the new token's Q (Query) vector changes. The KV cache stores these precomputed projections to avoid redundant recomputation, trading memory for compute. Without caching, generating the $n$-th token would require reprocessing all $n-1$ previous tokens, making generation cost quadratic in sequence length. With caching, each new token requires only one new K-V pair per layer, reducing cost to linear. The catch: for a 70B model with 80 layers and FP16 precision, the KV cache consumes ~1.3 MB per token per request, meaning a batch of 32 requests at 8,000 tokens each requires ~330 GB of KV cache alone—far exceeding the model weights themselves. +[^fn-kv-cache]: **KV Cache (Key-Value Cache)**: In transformer attention (introduced by Vaswani et al. [@vaswani2017attention]), each layer computes Key and Value projections from input tokens. During autoregressive generation, previously computed K and V vectors remain valid for all future tokens—only the new token's Q (Query) vector changes. The KV cache stores these precomputed projections to avoid redundant recomputation, trading memory for compute. Without caching, generating the $n$-th token would require reprocessing all $n-1$ previous tokens, making generation cost quadratic in sequence length. With caching, each new token requires only one new K-V pair per layer, reducing cost to linear. The catch: for a 70B model with 80 layers and FP16 precision, the KV cache consumes ~1.3 MB per token per request, meaning a batch of 32 requests at 8,000 tokens each requires ~330 GB of KV cache alone—far exceeding the model weights themselves. The continuous batching and PagedAttention techniques covered in @sec-model-serving-continuous-batching-8bb6 address these challenges. Advanced techniques including prefix caching and speculative decoding are covered in specialized coverage of large-scale systems. @@ -2931,15 +2933,15 @@ ONNX Runtime[^fn-onnx-runtime]\index{ONNX Runtime!cross-platform inference} prov TensorRT\index{Inference Engine!specialized}\index{TensorRT!GPU optimization}[^fn-tensorrt-serving] (NVIDIA GPUs), OpenVINO[^fn-openvino]\index{OpenVINO!Intel optimization} (Intel hardware), and similar engines optimize specifically for their target hardware [@nvidia2024tensorrt; @chen2018tvm]. They apply aggressive optimizations that framework-native runtimes cannot safely perform: -[^fn-openvino]: **OpenVINO (Open Visual Inference and Neural network Optimization)**: Intel's open-source toolkit, first released in May 2018. The name emphasizes its original focus on visual inference (computer vision on Intel hardware), though it now supports all neural network types. OpenVINO's core capability is mapping neural network operations onto Intel-specific instruction sets: AVX-512 and AMX (Advanced Matrix Extensions) on Xeon CPUs, integrated GPU execution via oneAPI, and specialized inference on Intel's Movidius VPUs (Vision Processing Units) for edge deployment. For organizations using Intel infrastructure, OpenVINO achieves 2--5$\times$ speedup over framework-native CPU inference through hardware-specific kernel implementations and INT8 calibration. +[^fn-openvino]: **OpenVINO (Open Visual Inference and Neural network Optimization)**: Intel's open-source toolkit, first released in May 2018. The name emphasizes its original focus on visual inference (computer vision on Intel hardware), though it now supports all neural network types. OpenVINO's core capability is mapping neural network operations onto Intel-specific instruction sets: AVX-512 and AMX (Advanced Matrix Extensions) on Xeon CPUs, integrated GPU execution via oneAPI, and specialized inference on Intel's Movidius VPUs (Vision Processing Units) for edge deployment. For organizations using Intel infrastructure, OpenVINO achieves 2--5 $\times$ speedup over framework-native CPU inference through hardware-specific kernel implementations and INT8 calibration. -[^fn-tensorrt-serving]: **TensorRT**: NVIDIA's inference optimization SDK that applies layer fusion, kernel auto-tuning, and precision calibration to neural networks. Unlike framework-native runtimes that preserve training-time graph structure, TensorRT rebuilds the computation graph for the specific target GPU during a build phase. This GPU-specific compilation means TensorRT engines are not portable across GPU architectures, requiring separate builds for V100, A100, and H100 deployments. The build phase can take minutes but produces engines that often achieve 2–5$\times$ speedup over PyTorch. +[^fn-tensorrt-serving]: **TensorRT**: NVIDIA's inference optimization SDK that applies layer fusion, kernel auto-tuning, and precision calibration to neural networks. Unlike framework-native runtimes that preserve training-time graph structure, TensorRT rebuilds the computation graph for the specific target GPU during a build phase. This GPU-specific compilation means TensorRT engines are not portable across GPU architectures, requiring separate builds for V100, A100, and H100 deployments. The build phase can take minutes but produces engines that often achieve 2–5 $\times$ speedup over PyTorch. -Layer fusion\index{Layer Fusion!kernel optimization} combines multiple sequential operations into a single GPU kernel. Consider a common pattern: convolution → batch normalization → ReLU activation. Without fusion, this requires three kernel launches, three round-trips to GPU memory (write conv output, read for batchnorm, write batchnorm output, read for ReLU), and three sets of intermediate tensors. Fusion combines all three into one kernel that reads inputs once, computes the combined result in registers, and writes final outputs once. This eliminates kernel launch overhead (15–60 μs saved per fusion) and reduces memory traffic by 2–3$\times$. TensorRT automatically detects and fuses common patterns; a typical ResNet-50 reduces from ~50 kernels to ~15 after fusion. +Layer fusion\index{Layer Fusion!kernel optimization} combines multiple sequential operations into a single GPU kernel. Consider a common pattern: convolution → batch normalization → ReLU activation. Without fusion, this requires three kernel launches, three round-trips to GPU memory (write conv output, read for batchnorm, write batchnorm output, read for ReLU), and three sets of intermediate tensors. Fusion combines all three into one kernel that reads inputs once, computes the combined result in registers, and writes final outputs once. This eliminates kernel launch overhead (15–60 μs saved per fusion) and reduces memory traffic by 2–3 $\times$. TensorRT automatically detects and fuses common patterns; a typical ResNet-50 reduces from ~50 kernels to ~15 after fusion. Kernel auto-tuning\index{Kernel Auto-Tuning!algorithm selection}\index{Algorithm Selection!convolution implementations} selects the fastest algorithm for each operation on the specific GPU. A single convolution can be implemented using dozens of algorithms (direct, FFT-based, Winograd, various tiling strategies), each optimal for different input sizes and GPU architectures. Auto-tuning benchmarks each candidate and caches the winner, trading compilation time for runtime performance. -These optimizations typically achieve 2–5$\times$ speedup over framework-native serving but require explicit export and may not support all operations. A *runtime comparison* on a standard model quantifies these gains across the optimization spectrum. +These optimizations typically achieve 2–5 $\times$ speedup over framework-native serving but require explicit export and may not support all operations. A *runtime comparison* on a standard model quantifies these gains across the optimization spectrum. ```{python} #| label: runtime-comparison-calc @@ -3000,16 +3002,16 @@ rt_trt_int8_speedup_str = fmt(rt_trt_int8_speedup_value, precision=1, commas=Fal Performance comparison for ResNet-50 inference on V100 GPU (batch size 1): -| **Runtime** | **Latency** | **Speedup** | **Notes** | -|:----------------|------------------------------------:|----------------------------------------------:|:--------------------------| -| PyTorch (eager) | `{python} rt_pytorch_ms_str` ms | `{python} rt_pytorch_speedup_str`$\times$ | Baseline, no optimization | -| TorchScript | `{python} rt_torchscript_ms_str` ms | `{python} rt_torchscript_speedup_str`$\times$ | JIT compilation | -| ONNX Runtime | `{python} rt_onnx_ms_str` ms | `{python} rt_onnx_speedup_str`$\times$ | Cross-platform | -| TensorRT FP32 | `{python} rt_trt_fp32_ms_str` ms | `{python} rt_trt_fp32_speedup_str`$\times$ | NVIDIA-specific | -| TensorRT FP16 | `{python} rt_trt_fp16_ms_str` ms | `{python} rt_trt_fp16_speedup_str`$\times$ | Tensor Core acceleration | -| TensorRT INT8 | `{python} rt_trt_int8_ms_str` ms | `{python} rt_trt_int8_speedup_str`$\times$ | Requires calibration | +| **Runtime** | **Latency** | **Speedup** | **Notes** | +|:----------------|------------------------------------:|-----------------------------------------------:|:--------------------------| +| PyTorch (eager) | `{python} rt_pytorch_ms_str` ms | `{python} rt_pytorch_speedup_str` $\times$ | Baseline, no optimization | +| TorchScript | `{python} rt_torchscript_ms_str` ms | `{python} rt_torchscript_speedup_str` $\times$ | JIT compilation | +| ONNX Runtime | `{python} rt_onnx_ms_str` ms | `{python} rt_onnx_speedup_str` $\times$ | Cross-platform | +| TensorRT FP32 | `{python} rt_trt_fp32_ms_str` ms | `{python} rt_trt_fp32_speedup_str` $\times$ | NVIDIA-specific | +| TensorRT FP16 | `{python} rt_trt_fp16_ms_str` ms | `{python} rt_trt_fp16_speedup_str` $\times$ | Tensor Core acceleration | +| TensorRT INT8 | `{python} rt_trt_int8_ms_str` ms | `{python} rt_trt_int8_speedup_str` $\times$ | Requires calibration | -**Key insight**: The `{python} rt_trt_int8_speedup_str`$\times$ speedup from TensorRT INT8 comes at the cost of: (1) quantization calibration data, (2) potential accuracy loss (<1% for ResNet-50), and (3) NVIDIA-specific deployment. +**Key insight**: The `{python} rt_trt_int8_speedup_str` $\times$ speedup from TensorRT INT8 comes at the cost of: (1) quantization calibration data, (2) potential accuracy loss (<1% for ResNet-50), and (3) NVIDIA-specific deployment. ::: @@ -3021,7 +3023,7 @@ Beyond runtime selection, configuration choices significantly impact serving per ### Precision Selection for Serving {#sec-model-serving-precision-selection-serving-55ba} -A team deploying ResNet-50 on V100 GPUs faces a concrete constraint: their 30-GPU cluster costs \$90/hour, and business growth requires 3$\times$ more throughput without expanding the fleet. Switching from FP32 to INT8 inference achieves exactly this—the same model on the same hardware serves 3$\times$ more requests per second, reducing the effective cost per inference by two-thirds, at a cost of less than 0.4 percentage points of accuracy. This example illustrates the direct connection between numerical precision and infrastructure economics. Precision selection connects to the quantization techniques covered in @sec-model-compression. For the foundational comparison of numerical formats (FP32, FP16, BF16, FP8, INT8) and their precision-range trade-offs, see @sec-machine-foundations-numerical-representations-c889; for the mechanics of symmetric and asymmetric integer quantization, see @sec-machine-foundations-integer-quantization-5442. While @sec-model-compression focuses on training-time quantization, serving introduces additional considerations including calibration requirements, layer sensitivity, and dynamic precision selection. +A team deploying ResNet-50 on V100 GPUs faces a concrete constraint: their 30-GPU cluster costs \$90/hour, and business growth requires 3 $\times$ more throughput without expanding the fleet. Switching from FP32 to INT8 inference achieves exactly this—the same model on the same hardware serves 3 $\times$ more requests per second, reducing the effective cost per inference by two-thirds, at a cost of less than 0.4 percentage points of accuracy. This example illustrates the direct connection between numerical precision and infrastructure economics. Precision selection connects to the quantization techniques covered in @sec-model-compression. For the foundational comparison of numerical formats (FP32, FP16, BF16, FP8, INT8) and their precision-range trade-offs, see @sec-machine-foundations-numerical-representations-c889; for the mechanics of symmetric and asymmetric integer quantization, see @sec-machine-foundations-integer-quantization-5442. While @sec-model-compression focuses on training-time quantization, serving introduces additional considerations including calibration requirements, layer sensitivity, and dynamic precision selection. #### Precision-Throughput Relationship {#sec-model-serving-precisionthroughput-relationship-b503} @@ -3031,7 +3033,7 @@ $$ \frac{\text{Throughput}_{\text{INT8}}}{\text{Throughput}_{\text{FP32}}} = \frac{32}{8} = 4\times \text{ (theoretical maximum)} $$ {#eq-precision-throughput} -In practice, GPU compute pipelines and Tensor Core alignment requirements limit achieved speedup to 2.5–3.5$\times$ for INT8 versus FP32. Tensor Cores\index{Tensor Cores!alignment requirements} require specific alignment: INT8 operations need tensor dimensions divisible by 16, while FP16 requires divisibility by 8. @sec-hardware-acceleration provides the detailed Tensor Core architecture that explains these alignment constraints. The *precision tradeoffs* for a standard vision model illustrate how these theoretical limits manifest in practice. +In practice, GPU compute pipelines and Tensor Core alignment requirements limit achieved speedup to 2.5–3.5 $\times$ for INT8 versus FP32. Tensor Cores\index{Tensor Cores!alignment requirements} require specific alignment: INT8 operations need tensor dimensions divisible by 16, while FP16 requires divisibility by 8. @sec-hardware-acceleration provides the detailed Tensor Core architecture that explains these alignment constraints. The *precision tradeoffs* for a standard vision model illustrate how these theoretical limits manifest in practice. ```{python} #| label: precision-tradeoff-calc @@ -3109,9 +3111,9 @@ pt_int8_acc_loss_str = f"{pt_int8_acc_loss_value:.2f}" **Key observations:** -- INT8 achieves `{python} pt_int8_speedup_str`$\times$ speedup but loses `{python} pt_int8_acc_loss_str`% accuracy with post-training quantization (PTQ) +- INT8 achieves `{python} pt_int8_speedup_str` $\times$ speedup but loses `{python} pt_int8_acc_loss_str`% accuracy with post-training quantization (PTQ) - Quantization-aware training (QAT) recovers most accuracy but requires retraining -- FP16 provides `{python} pt_fp16_speedup_str`$\times$ speedup with no accuracy loss for most models +- FP16 provides `{python} pt_fp16_speedup_str` $\times$ speedup with no accuracy loss for most models ::: @@ -3131,9 +3133,9 @@ Post-training\index{Calibration!INT8 quantization}\index{Post-Training Quantizat Advanced\index{Dynamic Precision!adaptive quality} serving systems select precision per request based on runtime conditions. If the system is ahead of latency SLO, it uses higher precision for better accuracy. For low-confidence INT8 results, it recomputes at FP16. Different customer tiers may receive different precision levels. This pattern enables adaptive quality-latency tradeoffs while maximizing throughput during normal operation. -The precision decision has direct infrastructure consequences: INT8 inference achieves roughly 3$\times$ higher throughput than FP32, meaning a workload requiring 30 GPUs at FP32 needs only 10 at INT8. This 3$\times$ reduction in hardware translates directly to a 3$\times$ reduction in operating costs. The connection between model-level optimization and infrastructure economics is why precision selection cannot be treated as purely a model concern. +The precision decision has direct infrastructure consequences: INT8 inference achieves roughly 3 $\times$ higher throughput than FP32, meaning a workload requiring 30 GPUs at FP32 needs only 10 at INT8. This 3 $\times$ reduction in hardware translates directly to a 3 $\times$ reduction in operating costs. The connection between model-level optimization and infrastructure economics is why precision selection cannot be treated as purely a model concern. -Runtime selection and precision tuning operate at the model level: they determine *what* computation runs and at *what* numerical format. But between the model and the silicon lies another optimization layer—the mechanics of how computation graphs compile to kernels, how bytes move from disk to memory, and how the CPU and GPU coordinate their work. These node-level techniques often yield the final 2–5$\times$ that separates a functional prototype from a production-grade serving node. +Runtime selection and precision tuning operate at the model level: they determine *what* computation runs and at *what* numerical format. But between the model and the silicon lies another optimization layer—the mechanics of how computation graphs compile to kernels, how bytes move from disk to memory, and how the CPU and GPU coordinate their work. These node-level techniques often yield the final 2–5 $\times$ that separates a functional prototype from a production-grade serving node. ## Node-Level Optimization {#sec-model-serving-nodelevel-optimization-3d9d} @@ -3141,7 +3143,7 @@ Runtime selection and precision tuning establish the software foundation for ser ### Runtime Graph Compilation {#sec-model-serving-runtime-graph-compilation-7a7e} -Inference engines like TensorRT were introduced in @sec-model-serving-inference-runtime-selection-5eef. How do they achieve 2–5$\times$ speedups? The answer lies in **Graph Compilation**. Training computation graphs are dynamic and mutable, whereas serving graphs are static. This static nature allows compilers to perform aggressive optimizations that would be unsafe or too slow during training. +Inference engines like TensorRT were introduced in @sec-model-serving-inference-runtime-selection-5eef. How do they achieve 2–5 $\times$ speedups? The answer lies in **Graph Compilation**. Training computation graphs are dynamic and mutable, whereas serving graphs are static. This static nature allows compilers to perform aggressive optimizations that would be unsafe or too slow during training. #### Operator Fusion {#sec-model-serving-operator-fusion-f8d2} @@ -3172,7 +3174,7 @@ GPUs dominate the narrative, yet CPUs\index{CPU Inference!when to use} remain th Modern CPUs[^fn-simd-serving]\index{SIMD!CPU vectorization}\index{AVX-512!vector instructions} (Intel Xeon, AMD EPYC) pack powerful vector units (AVX-512, AMX). Standard Python loops cannot use these. Specialized runtimes like **OpenVINO** or **Intel Extension for PyTorch (IPEX)** map neural network operators directly to these vector instructions, achieving order-of-magnitude speedups over vanilla implementations. -[^fn-simd-serving]: **SIMD (Single Instruction, Multiple Data)**: A parallel computing classification from Michael Flynn's 1966 taxonomy of computer architectures. SIMD enables one instruction to operate on multiple data elements simultaneously—for example, adding eight pairs of floating-point numbers in a single clock cycle. Intel's AVX-512 (Advanced Vector Extensions, 2016) processes 512 bits (16 floats) per instruction; AMX (Advanced Matrix Extensions, 2023) extends this to matrix tile operations. For CPU inference, SIMD exploitation is the primary lever: a naively written matrix multiplication uses scalar operations at ~1% of theoretical peak, while SIMD-optimized kernels approach 80--90% utilization. +[^fn-simd-serving]: **SIMD (Single Instruction, Multiple Data)**: A parallel computing classification from Michael Flynn's 1966 taxonomy [@flynn1966very] of computer architectures. SIMD enables one instruction to operate on multiple data elements simultaneously—for example, adding eight pairs of floating-point numbers in a single clock cycle. Intel's AVX-512 (Advanced Vector Extensions, 2016) processes 512 bits (16 floats) per instruction; AMX (Advanced Matrix Extensions, 2023) extends this to matrix tile operations. For CPU inference, SIMD exploitation is the primary lever: a naively written matrix multiplication uses scalar operations at ~1% of theoretical peak, while SIMD-optimized kernels approach 80--90% utilization. #### Thread Pinning and NUMA {#sec-model-serving-thread-pinning-numa-dc2b} @@ -3209,7 +3211,7 @@ Loading a 5GB Stable Diffusion model: By using `mmap` and formats like `safetensors`, loading speed becomes limited only by the disk's read speed (e.g., 3GB/s for NVMe), rather than CPU parsing overhead. ::: -[^fn-safetensors]: **Safetensors**: Created by Hugging Face engineer Nicolas Patry and released in 2022. The name emphasizes safety: unlike Python's pickle format (used by `torch.save`), safetensors cannot execute arbitrary code during deserialization, eliminating a class of security vulnerabilities where malicious model files could compromise a serving system. The format stores tensors as contiguous raw bytes with a small JSON header describing shapes and data types, enabling memory-mapped loading where the OS maps the file directly into the process's address space. This design achieves 30--100$\times$ faster loading than pickle because no parsing, object construction, or memory copying occurs. +[^fn-safetensors]: **Safetensors**: Created by Hugging Face engineer Nicolas Patry and released in 2022. The name emphasizes safety: unlike Python's pickle format (used by `torch.save`), safetensors cannot execute arbitrary code during deserialization, eliminating a class of security vulnerabilities where malicious model files could compromise a serving system. The format stores tensors as contiguous raw bytes with a small JSON header describing shapes and data types, enabling memory-mapped loading where the OS maps the file directly into the process's address space. This design achieves 30--100 $\times$ faster loading than pickle because no parsing, object construction, or memory copying occurs. ### Profiling the Serving Node {#sec-model-serving-profiling-serving-node-1e99} @@ -3237,10 +3239,10 @@ To guide optimization efforts, @tbl-optimization-impact summarizes the key techn | **Technique** | **Target Metric** | **Typical Gain** | **Implement. Cost** | **Best For** | |:----------------------|:---------------------|-----------------:|:--------------------|:-------------------------| -| **Operator Fusion** | Latency & Throughput | 2–5$\times$ | Medium (Compiler) | Memory-bound layers | -| **INT8 Quantization** | Throughput | 3–4$\times$ | High (Calibration) | Inference-heavy nodes | -| **Graph Compilation** | Latency | 1.5–3$\times$ | Low (One-line) | Static graph models | -| **Zero-Copy Loading** | Startup Time | 10–50$\times$ | Low (File format) | Autoscaling / Cold Start | +| **Operator Fusion** | Latency & Throughput | 2–5 $\times$ | Medium (Compiler) | Memory-bound layers | +| **INT8 Quantization** | Throughput | 3–4 $\times$ | High (Calibration) | Inference-heavy nodes | +| **Graph Compilation** | Latency | 1.5–3 $\times$ | Low (One-line) | Static graph models | +| **Zero-Copy Loading** | Startup Time | 10–50 $\times$ | Low (File format) | Autoscaling / Cold Start | | **CPU Pinning** | Tail Latency (P99) | 20-50% reduction | Low (Config) | Latency-critical apps | : **Node-Level Optimization Impact**: A decision matrix for selecting optimization techniques. High-impact techniques like quantization often carry higher implementation costs (calibration data requirements), while architectural changes like zero-copy loading offer dramatic gains for specific metrics (startup time) with low effort. {#tbl-optimization-impact} @@ -3332,7 +3334,7 @@ Consider serving ResNet-50 on AWS infrastructure (US-East region, on-demand pric | **g4dn.xlarge (T4 GPU)** | \$`{python} ca_t4_cost_str` | `{python} ca_t4_throughput_str` img/s | \$`{python} ca_t4_cpm_str` | | **p3.2xlarge (V100 GPU)** | \$`{python} ca_v100_cost_str` | `{python} ca_v100_throughput_str` img/s | \$`{python} ca_v100_cpm_str` | -**Key insight**: The T4 GPU instance achieves the lowest cost per inference despite higher hourly cost, because GPU throughput dramatically exceeds CPU throughput. The V100 is only cost-effective at very high sustained traffic where its higher throughput justifies the `{python} ca_v100_price_increase_str`$\times$ price increase. Note that cloud pricing varies by region and changes over time; consult current pricing for production planning. +**Key insight**: The T4 GPU instance achieves the lowest cost per inference despite higher hourly cost, because GPU throughput dramatically exceeds CPU throughput. The V100 is only cost-effective at very high sustained traffic where its higher throughput justifies the `{python} ca_v100_price_increase_str` $\times$ price increase. Note that cloud pricing varies by region and changes over time; consult current pricing for production planning. ::: @@ -3413,9 +3415,10 @@ The linear growth of the KV cache with sequence length forces a hard trade-off: #### Workload Profile {#sec-model-serving-workload-profile-a380} * **Model**: Llama-3-8B (quantized to 4-bit AWQ[^fn-awq]\index{AWQ!4-bit quantization}). -[^fn-awq]: **AWQ (Activation-Aware Weight Quantization)**: Introduced by Ji Lin et al. at MIT in 2023. AWQ observes that not all weights contribute equally to model quality—weights corresponding to channels with large activation magnitudes are disproportionately important. Rather than quantizing all weights identically, AWQ applies per-channel scaling that protects salient weights from quantization error while aggressively compressing less important ones. This activation-aware approach achieves 4-bit quantization with negligible quality loss (typically <0.5% accuracy degradation), enabling models to fit in roughly 4$\times$ less GPU memory and achieving proportional memory bandwidth savings during the decode phase. For serving, AWQ's 4-bit compression directly translates to higher concurrent batch sizes and lower cost per token. -* **Hardware**: 1$\times$ NVIDIA H100 SXM5 GPU (`{python} h100_mem` GB HBM3, `{python} h100_bw_tbs` TB/s bandwidth). +[^fn-awq]: **AWQ (Activation-Aware Weight Quantization)**: Introduced by Ji Lin et al. at MIT in 2023. AWQ observes that not all weights contribute equally to model quality—weights corresponding to channels with large activation magnitudes are disproportionately important. Rather than quantizing all weights identically, AWQ applies per-channel scaling that protects salient weights from quantization error while aggressively compressing less important ones. This activation-aware approach achieves 4-bit quantization with negligible quality loss (typically <0.5% accuracy degradation), enabling models to fit in roughly 4 $\times$ less GPU memory and achieving proportional memory bandwidth savings during the decode phase. For serving, AWQ's 4-bit compression directly translates to higher concurrent batch sizes and lower cost per token. + +* **Hardware**: 1 $\times$ NVIDIA H100 SXM5 GPU (`{python} h100_mem` GB HBM3, `{python} h100_bw_tbs` TB/s bandwidth). * **Request Characteristics**: 1,000-token input prompt (Prefill), 256-token generated response (Decode). * **Target SLOs**: TTFT $<$ 200 ms, TPOT $<$ 20 ms. @@ -3433,7 +3436,7 @@ The model generates 256 tokens sequentially. This phase is memory-bandwidth boun ::: {.callout-perspective title="The Physics of Token Generation"} -Recall the **Energy-Movement Invariant** from @sec-data-engineering: moving a bit is 100–1,000$\times$ more expensive than computing on it. In the **Decode Phase**, this law determines the physical "cost per word." +Recall the **Energy-Movement Invariant** from @sec-data-engineering: moving a bit is 100–1,000 $\times$ more expensive than computing on it. In the **Decode Phase**, this law determines the physical "cost per word." **The Memory Wall for Generative AI**: Because the decode phase has an arithmetic intensity of $\approx 1$ FLOP/byte (we must read every weight just to generate one token), performance is strictly limited by memory bandwidth ($BW$), not compute. This relationship is captured in @eq-token-generation-time: @@ -3524,7 +3527,7 @@ remaining_vram_gb_str = f"{remaining_vram_gb_value}" With 4-bit weights occupying `{python} model_weight_gb_str` GB, the remaining ~`{python} remaining_vram_gb_str` GB of VRAM is available for the **KV Cache**. Using **PagedAttention**, we can allocate this memory with near-zero fragmentation. -* Each token requires approximately `{python} kv_per_token_mb_str` MB of KV cache (32 layers $\times$ 4096 dim $\times$ 2 vectors $\times$ 2-byte precision, assuming standard multi-head attention; models with Grouped Query Attention use fewer KV heads, reducing this by up to 4$\times$). +* Each token requires approximately `{python} kv_per_token_mb_str` MB of KV cache (32 layers $\times$ 4096 dim $\times$ 2 vectors $\times$ 2-byte precision, assuming standard multi-head attention; models with Grouped Query Attention use fewer KV heads, reducing this by up to 4 $\times$). * Total cache capacity ≈ `{python} kv_cache_gb_str` GB / `{python} kv_per_token_mb_str` MB/token ≈ `{python} kv_capacity_tokens_str` tokens. * At `{python} tokens_per_req_str` tokens per request (input + output), the GPU can handle a **concurrent batch size of ~`{python} concurrent_batch_str` requests**. @@ -3611,7 +3614,7 @@ fl_system_speedup_str = f"{fl_system_speedup_value:.1f}" fl_model_speedup_str = f"{fl_model_speedup_value:.1f}" ``` -Engineers who optimize model inference expect proportional improvement in user-perceived latency\index{Latency!inference vs user-perceived}, but serving systems introduce latency sources absent from offline benchmarks. Under load, queuing delay dominates: @eq-mm1-wait shows that at `{python} fl_utilization_high_pct_str` percent utilization with `{python} fl_service_slow_ms_str`ms service time, average wait time is `{python} fl_wait_slow_ms_str`ms before inference even begins. Reducing inference from `{python} fl_service_slow_ms_str`ms to `{python} fl_service_fast_ms_str`ms changes service time but also shifts utilization from `{python} fl_utilization_high_pct_str` percent to `{python} fl_utilization_new_pct_str` percent, reducing queuing wait from `{python} fl_wait_slow_ms_str`ms to `{python} fl_wait_fast_ms_str`ms, a `{python} fl_queuing_improvement_str`$\times$ queuing improvement that dwarfs the `{python} fl_inference_gain_ms_str`ms inference gain. This nonlinear interaction between inference speed and queuing behavior means the *system-level* speedup (`{python} fl_total_slow_ms_str`ms → `{python} fl_total_fast_ms_str`ms, or `{python} fl_system_speedup_str`$\times$) far exceeds the *model-level* speedup (`{python} fl_service_slow_ms_str`ms → `{python} fl_service_fast_ms_str`ms, or `{python} fl_model_speedup_str`$\times$). Conversely, teams that reduce inference by only 20 percent at high utilization see negligible user-facing improvement because queuing still dominates. Serving optimization requires analyzing the complete latency budget, including serialization, queuing, preprocessing, and postprocessing, under realistic load conditions rather than profiling inference latency in isolation. +Engineers who optimize model inference expect proportional improvement in user-perceived latency\index{Latency!inference vs user-perceived}, but serving systems introduce latency sources absent from offline benchmarks. Under load, queuing delay dominates: @eq-mm1-wait shows that at `{python} fl_utilization_high_pct_str` percent utilization with `{python} fl_service_slow_ms_str`ms service time, average wait time is `{python} fl_wait_slow_ms_str`ms before inference even begins. Reducing inference from `{python} fl_service_slow_ms_str`ms to `{python} fl_service_fast_ms_str`ms changes service time but also shifts utilization from `{python} fl_utilization_high_pct_str` percent to `{python} fl_utilization_new_pct_str` percent, reducing queuing wait from `{python} fl_wait_slow_ms_str`ms to `{python} fl_wait_fast_ms_str`ms, a `{python} fl_queuing_improvement_str` $\times$ queuing improvement that dwarfs the `{python} fl_inference_gain_ms_str`ms inference gain. This nonlinear interaction between inference speed and queuing behavior means the *system-level* speedup (`{python} fl_total_slow_ms_str`ms → `{python} fl_total_fast_ms_str`ms, or `{python} fl_system_speedup_str` $\times$) far exceeds the *model-level* speedup (`{python} fl_service_slow_ms_str`ms → `{python} fl_service_fast_ms_str`ms, or `{python} fl_model_speedup_str` $\times$). Conversely, teams that reduce inference by only 20 percent at high utilization see negligible user-facing improvement because queuing still dominates. Serving optimization requires analyzing the complete latency budget, including serialization, queuing, preprocessing, and postprocessing, under realistic load conditions rather than profiling inference latency in isolation. **Pitfall:** *Running serving infrastructure at high utilization to maximize cost efficiency.* @@ -3670,7 +3673,7 @@ fu_p99_mod_ms_str = f"{fu_p99_mod_value:.0f}" # ~77 fu_p99_high_ms_str = f"{fu_p99_high_value:.0f}" # ~230 ``` -Teams target `{python} fu_util_high_pct_str` percent utilization\index{Utilization!high utilization pitfall} to minimize idle capacity. In production, latency degrades nonlinearly as utilization approaches capacity. @eq-mm1-wait shows that at `{python} fu_util_high_pct_str` percent utilization, average time in system reaches `{python} fu_total_high_factor_str`$\times$ service time. Moving from `{python} fu_util_mod_pct_str` percent to `{python} fu_util_high_pct_str` percent utilization cuts infrastructure costs by `{python} fu_cost_reduction_str` percent but triples average latency. For a `{python} fu_service_ms_str`ms inference service, p99 latency jumps from ~`{python} fu_p99_mod_ms_str`ms to ~`{python} fu_p99_high_ms_str`ms (M/M/1 model). Systems provisioned for average load violate SLOs precisely when traffic increases during business-critical periods. Production systems targeting 60 to 70 percent utilization at peak load maintain the latency headroom needed to absorb traffic spikes. +Teams target `{python} fu_util_high_pct_str` percent utilization\index{Utilization!high utilization pitfall} to minimize idle capacity. In production, latency degrades nonlinearly as utilization approaches capacity. @eq-mm1-wait shows that at `{python} fu_util_high_pct_str` percent utilization, average time in system reaches `{python} fu_total_high_factor_str` $\times$ service time. Moving from `{python} fu_util_mod_pct_str` percent to `{python} fu_util_high_pct_str` percent utilization cuts infrastructure costs by `{python} fu_cost_reduction_str` percent but triples average latency. For a `{python} fu_service_ms_str`ms inference service, p99 latency jumps from ~`{python} fu_p99_mod_ms_str`ms to ~`{python} fu_p99_high_ms_str`ms (M/M/1 model). Systems provisioned for average load violate SLOs precisely when traffic increases during business-critical periods. Production systems targeting 60 to 70 percent utilization at peak load maintain the latency headroom needed to absorb traffic spikes. **Fallacy:** *Training accuracy guarantees serving accuracy.* @@ -3754,7 +3757,7 @@ tl_p99_ms_str = f"{tl_p99_ms_value:.0f}" tl_gap_str = f"{tl_gap_value:.1f}" ``` -Engineers monitor average latency\index{Mean Latency!monitoring pitfall} because it trends smoothly and is simple to compute. In production, averages hide the slowest requests that determine user satisfaction. As @sec-model-serving-tail-latency-5376 demonstrates, at `{python} tl_util_pct_str` percent utilization with `{python} tl_service_ms_str`ms service time, average latency is `{python} tl_avg_ms_str`ms while p99 reaches `{python} tl_p99_ms_str`ms, a `{python} tl_gap_str`$\times$ gap invisible to mean-based monitoring. Teams optimizing average latency miss the tail that determines user satisfaction: the 1 percent of users experiencing `{python} tl_p99_ms_str`ms delays often generate the most valuable transactions. Production SLOs specify percentile targets (p95, p99) precisely because averages mask tail behavior. +Engineers monitor average latency\index{Mean Latency!monitoring pitfall} because it trends smoothly and is simple to compute. In production, averages hide the slowest requests that determine user satisfaction. As @sec-model-serving-tail-latency-5376 demonstrates, at `{python} tl_util_pct_str` percent utilization with `{python} tl_service_ms_str`ms service time, average latency is `{python} tl_avg_ms_str`ms while p99 reaches `{python} tl_p99_ms_str`ms, a `{python} tl_gap_str` $\times$ gap invisible to mean-based monitoring. Teams optimizing average latency miss the tail that determines user satisfaction: the 1 percent of users experiencing `{python} tl_p99_ms_str`ms delays often generate the most valuable transactions. Production SLOs specify percentile targets (p95, p99) precisely because averages mask tail behavior. **Fallacy:** *Larger serving batches always improve throughput without affecting latency SLOs.* @@ -3883,7 +3886,7 @@ cs_cold_latency_str = f"{cs_cold_latency_ms_value}" cs_cold_mult_str = f"{cs_cold_multiplier_value:.0f}" ``` -Engineers optimize steady-state latency\index{Cold Start!bursty traffic impact} assuming most requests hit warm instances. In production, cold starts compound during the events that matter most: traffic spikes requiring scale-up, deployments rolling out new versions, and recovery from instance failures. @sec-model-serving-model-loading-initialization-cc5a details the anatomy of cold start: TensorRT compilation alone takes `{python} cs_compile_time_str` seconds per instance. During a traffic spike requiring `{python} cs_new_instances_str` new instances, aggregate cold start latency reaches `{python} cs_aggregate_cold_str` seconds of user-facing delay before new capacity becomes useful. Worse, requests hitting cold instances experience `{python} cs_cold_latency_str` ms latency versus `{python} cs_steady_latency_str` ms steady-state, a `{python} cs_cold_mult_str`$\times$ degradation that violates SLOs precisely when traffic is highest. Systems ignoring cold start meet SLOs during steady state but fail during scale-up events and deployment windows when reliability matters most. +Engineers optimize steady-state latency\index{Cold Start!bursty traffic impact} assuming most requests hit warm instances. In production, cold starts compound during the events that matter most: traffic spikes requiring scale-up, deployments rolling out new versions, and recovery from instance failures. @sec-model-serving-model-loading-initialization-cc5a details the anatomy of cold start: TensorRT compilation alone takes `{python} cs_compile_time_str` seconds per instance. During a traffic spike requiring `{python} cs_new_instances_str` new instances, aggregate cold start latency reaches `{python} cs_aggregate_cold_str` seconds of user-facing delay before new capacity becomes useful. Worse, requests hitting cold instances experience `{python} cs_cold_latency_str` ms latency versus `{python} cs_steady_latency_str` ms steady-state, a `{python} cs_cold_mult_str` $\times$ degradation that violates SLOs precisely when traffic is highest. Systems ignoring cold start meet SLOs during steady state but fail during scale-up events and deployment windows when reliability matters most. ## Summary {#sec-model-serving-summary-9635} @@ -3891,17 +3894,17 @@ Serving marks the transition from model development to production deployment, wh Effective serving optimization requires understanding the complete request path rather than focusing exclusively on model inference. Interface protocols like gRPC and efficient serialization formats minimize the "tax" of data movement, while preprocessing often consumes 45 to 70 percent of total latency when inference runs on optimized accelerators. The microsecond-scale overheads identified by Barroso, Patterson, and colleagues explain *why* serving latency often exceeds the sum of its measured parts, and *why* system-level optimization matters as much as model optimization. Training-serving skew represents another dimension of this complexity, silently degrading accuracy when preprocessing logic differs between training and production environments in ways that traditional testing cannot detect. -The traffic pattern analysis reveals *how* the deployment paradigm selected in @sec-ml-systems shapes every serving decision downstream. Server workloads with Poisson arrivals optimize dynamic batching windows, autonomous vehicles with streaming sensor data require synchronized batch formation, and mobile applications with single-user patterns eliminate batching entirely—each pattern a direct consequence of the physical constraints (power wall, memory wall, light barrier) that created the four paradigms in the first place. The MLPerf scenarios codify these patterns for standardized benchmarking, connecting the serving principles established here to the measurement frameworks explored in @sec-benchmarking. Node-level optimization techniques—graph compilation, operator fusion, and systematic profiling—bridge the gap between model-level decisions and hardware execution, often yielding 2–5$\times$ additional speedup through better utilization of the accelerator's duty cycle. Precision selection and runtime optimization extend the quantization techniques from @sec-model-compression and Tensor Core capabilities from @sec-hardware-acceleration into the serving domain. Finally, the translation of these technical metrics into unit economics, as shown by the Llama-3 case study, demonstrates *how* engineering decisions regarding batching, precision, and hardware selection directly determine the financial viability of deployment—a pressure intensified by the intelligence deflation trend (@fig-intelligence-deflation) that continually compresses per-inference margins. +The traffic pattern analysis reveals *how* the deployment paradigm selected in @sec-ml-systems shapes every serving decision downstream. Server workloads with Poisson arrivals optimize dynamic batching windows, autonomous vehicles with streaming sensor data require synchronized batch formation, and mobile applications with single-user patterns eliminate batching entirely—each pattern a direct consequence of the physical constraints (power wall, memory wall, light barrier) that created the four paradigms in the first place. The MLPerf scenarios codify these patterns for standardized benchmarking, connecting the serving principles established here to the measurement frameworks explored in @sec-benchmarking. Node-level optimization techniques—graph compilation, operator fusion, and systematic profiling—bridge the gap between model-level decisions and hardware execution, often yielding 2–5 $\times$ additional speedup through better utilization of the accelerator's duty cycle. Precision selection and runtime optimization extend the quantization techniques from @sec-model-compression and Tensor Core capabilities from @sec-hardware-acceleration into the serving domain. Finally, the translation of these technical metrics into unit economics, as shown by the Llama-3 case study, demonstrates *how* engineering decisions regarding batching, precision, and hardware selection directly determine the financial viability of deployment—a pressure intensified by the intelligence deflation trend (@fig-intelligence-deflation) that continually compresses per-inference margins. ::: {.callout-takeaways title="Inverting Every Training Priority"} * **Serving inverts training priorities**: Training optimizes throughput (samples/hour); serving optimizes latency (ms/request). Different objectives require different system designs. -* **Queuing theory governs capacity planning**: At 80% utilization, wait time is 5$\times$ service time; at 90%, it reaches 10$\times$. Small load increases cause disproportionate latency spikes. +* **Queuing theory governs capacity planning**: At 80% utilization, wait time is 5 $\times$ service time; at 90%, it reaches 10 $\times$. Small load increases cause disproportionate latency spikes. * **Preprocessing dominates optimized systems**: When model inference is fast (5ms), preprocessing (image decode, tokenization) consumes 45–70% of total latency. Optimize the pipeline, not just the model. * **Batching strategy depends on traffic pattern**: Poisson arrivals (web APIs) use dynamic batching; streaming sensors use synchronized batches; mobile apps eliminate batching entirely. * **Training-serving skew can degrade accuracy undetected**: Different preprocessing between training and serving (e.g., resize interpolation, normalization order) shifts inputs outside the training distribution, causing accuracy degradation that conventional monitoring cannot detect. Use identical code paths. -* **LLM serving is memory-bandwidth bound**: Token generation reads the entire model from VRAM per token, making decode latency strictly limited by memory bandwidth rather than compute. KV cache management via PagedAttention and continuous batching is the primary throughput lever, achieving 2–4$\times$ improvement over naive serving. -* **Precision and runtime selection directly determine infrastructure cost**: INT8 inference achieves ~3$\times$ higher throughput than FP32, translating directly to proportionally fewer GPUs. Runtime optimization (TensorRT, ONNX Runtime) provides an additional 2–5$\times$ speedup over framework-native serving, making these choices as impactful as model architecture decisions. +* **LLM serving is memory-bandwidth bound**: Token generation reads the entire model from VRAM per token, making decode latency strictly limited by memory bandwidth rather than compute. KV cache management via PagedAttention and continuous batching is the primary throughput lever, achieving 2–4 $\times$ improvement over naive serving. +* **Precision and runtime selection directly determine infrastructure cost**: INT8 inference achieves ~3 $\times$ higher throughput than FP32, translating directly to proportionally fewer GPUs. Runtime optimization (TensorRT, ONNX Runtime) provides an additional 2–5 $\times$ speedup over framework-native serving, making these choices as impactful as model architecture decisions. ::: diff --git a/book/quarto/contents/vol1/nn_architectures/images/png/comm_primitives.png b/book/quarto/contents/vol1/nn_architectures/images/png/comm_primitives.png new file mode 100644 index 0000000000000000000000000000000000000000..06c88d6da80d7260d841ad4d641656fdabb9c6f9 GIT binary patch literal 319119 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 VH8%M=9`lzbFc%ng<>@ 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'-x2R&x2_ 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/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 Gz5fRw;Z~IZ literal 0 HcmV?d00001 diff --git a/book/quarto/contents/vol1/nn_architectures/nn_architectures.qmd b/book/quarto/contents/vol1/nn_architectures/nn_architectures.qmd index 1058756c63..d75e8cebe9 100644 --- a/book/quarto/contents/vol1/nn_architectures/nn_architectures.qmd +++ b/book/quarto/contents/vol1/nn_architectures/nn_architectures.qmd @@ -21,7 +21,7 @@ engine: jupyter ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{30}{15}{90}{25}{15}{0}{10}{10,0,55,20,10,0,10} +\mlsysstack{40}{25}{90}{35}{15}{0}{10}{10} \end{marginfigure} _Why is choosing a neural network architecture an infrastructure commitment rather than a modeling decision?_ @@ -183,7 +183,7 @@ The Deep Learning Recommendation Model (DLRM) was open-sourced by Meta to expose \index{Depthwise Separable Convolution!MobileNet} **MobileNet (Google, 2017)**\index{MobileNet} -MobileNet [@howard2017mobilenets] challenged the trend of ever-larger models by prioritizing efficiency. It introduced depthwise separable convolutions, an architectural innovation that reduced computational cost (FLOPs) by 8–9$\times$ for $3 \times 3$ kernels with minimal accuracy loss, making it a prime candidate for quantization techniques covered in @sec-model-compression. It proved that model architecture could be co-designed with hardware constraints, becoming the standard for running vision models on smartphones and embedded devices where battery life and latency are critical. +MobileNet [@howard2017mobilenets] challenged the trend of ever-larger models by prioritizing efficiency. It introduced depthwise separable convolutions, an architectural innovation that reduced computational cost (FLOPs) by 8–9 $\times$ for $3 \times 3$ kernels with minimal accuracy loss, making it a prime candidate for quantization techniques covered in @sec-model-compression. It proved that model architecture could be co-designed with hardware constraints, becoming the standard for running vision models on smartphones and embedded devices where battery life and latency are critical. **Keyword Spotting (KWS) (Google/Arm, 2017)**\index{Keyword Spotting (KWS)} Keyword Spotting models (like those detecting "Hey Siri" or "Ok Google") represent the extreme end of efficiency. Designed to run on "always-on" microcontrollers with kilobyte-scale memory and milliwatt power budgets, these models (often Depthwise Separable CNNs) exemplify the constraints of TinyML. They force engineers to count every byte and cycle, driving innovations in extreme quantization (int8/int4) and specialized hardware. @@ -343,12 +343,14 @@ We begin with the simplest architecture in our spectrum. Multi-Layer Perceptrons \index{Rosenblatt, Frank} \index{McCulloch-Pitts Neuron} + [^fn-perceptron-etymology]: **Perceptron**\index{Perceptron}: Coined by Frank Rosenblatt at Cornell in 1957, combining "perception" with the suffix "-tron" (electronic device), following the naming convention of era machines like cyclotron and magnetron. Rosenblatt described the perceptron as "the first machine capable of having an original idea," building on McCulloch and Pitts' 1943 neuron model to create what he envisioned as a computational model for biological perception and memory. MLPs embody an inductive bias: **they assume no prior structure in the data, allowing any input to relate to any output**. This architectural choice enables maximum flexibility by treating all input relationships as equally plausible, making MLPs versatile but computationally intensive compared to specialized alternatives. Their computational power was established theoretically by the Universal Approximation Theorem (UAT)[^fn-uat] [@cybenko1989approximation; @hornik1989multilayer], which we encountered as a footnote in @sec-neural-computation. This theorem states that a sufficiently large MLP with non-linear activation functions can approximate any continuous function on a compact domain, given suitable weights and biases. The following definition formalizes the *multi-layer perceptron* as an architectural concept. \index{Cybenko, George} -[^fn-uat]: **Universal Approximation Theorem**\index{Universal Approximation Theorem}: Proven independently by Cybenko (1989) and Hornik (1989), this result showed that neural networks could theoretically learn any function, a discovery that reinvigorated interest in neural networks after the "AI Winter" of the 1980s and established mathematical foundations for modern deep learning. + +[^fn-uat]: **Universal Approximation Theorem**\index{Universal Approximation Theorem}: Proven independently by Cybenko [@cybenko1989approximation] and Hornik [@hornik1989multilayer], this result showed that neural networks could theoretically learn any function, a discovery that reinvigorated interest in neural networks after the "AI Winter" of the 1980s and established mathematical foundations for modern deep learning. ::: {.callout-definition title="Multi-Layer Perceptrons"} @@ -415,7 +417,7 @@ This distinction resolves what appears to be a paradox: if MLPs are universal ap Three factors create the learnability gap: -1. **Sample complexity**\index{Sample Complexity}: The UAT provides no bounds on training examples needed. For 28$\times$28 images, an MLP treats `{python} MLPDim.mlp_28_dim_str` pixels independently, requiring exponentially many samples to learn spatial correlations. A CNN embeds locality bias, drastically reducing sample requirements. Mathematically, sample complexity can scale as $O(\exp(d))$ for MLPs but $O(\text{poly}(d))$ for architectures matching data structure. +1. **Sample complexity**\index{Sample Complexity}: The UAT provides no bounds on training examples needed. For 28 $\times$ 28 images, an MLP treats `{python} MLPDim.mlp_28_dim_str` pixels independently, requiring exponentially many samples to learn spatial correlations. A CNN embeds locality bias, drastically reducing sample requirements. Mathematically, sample complexity can scale as $O(\exp(d))$ for MLPs but $O(\text{poly}(d))$ for architectures matching data structure. 2. **Parameter efficiency**\index{Network Width}: The UAT guarantees *some* width suffices, but provides no constructive bounds. Required width can be exponential in input dimension: approximating $\sin(x_1) + \cdots + \sin(x_d)$ may require $O(\exp(d))$ MLP neurons versus $O(d)$ for architectures processing dimensions independently. @@ -498,7 +500,7 @@ Consider classifying $28 \times 28$ MNIST digits (784 input pixels, 10 output cl **CNN Approach**: -- Architecture: Conv(32, 3$\times$3) → Pool → Conv(64, 3$\times$3) → Pool → FC(128) → 10 +- Architecture: Conv(32, 3 $\times$ 3) → Pool → Conv(64, 3 $\times$ 3) → Pool → FC(128) → 10 - Parameters: (3 $\times$ 3 $\times$ 1 $\times$ 32) + (3 $\times$ 3 $\times$ 32 $\times$ 64) + (64 $\times$ 7 $\times$ 7 $\times$ 128) + (128 $\times$ 10) ≈ `{python} MLPvsCNN.cnn_params_str` parameters - Training: 60,000 examples (same data) - Test Accuracy: ~99%+ @@ -506,9 +508,9 @@ Consider classifying $28 \times 28$ MNIST digits (784 input pixels, 10 output cl **Comparison**: -- Parameter Efficiency: CNN uses `{python} MLPvsCNN.param_ratio_str`$\times$ fewer parameters +- Parameter Efficiency: CNN uses `{python} MLPvsCNN.param_ratio_str` $\times$ fewer parameters - Sample Efficiency: CNN achieves better accuracy with the same training data -- Systems Implications: CNN requires `{python} MLPvsCNN.param_ratio_str`$\times$ less memory, trains faster, and runs faster at inference +- Systems Implications: CNN requires `{python} MLPvsCNN.param_ratio_str` $\times$ less memory, trains faster, and runs faster at inference Both architectures can *represent* the digit classification function (UAT guarantees this for MLPs; CNNs have similar or greater representational capacity). The difference is *learnability*: the CNN's inductive bias matches the spatial structure of images, enabling efficient learning with limited data and compute. ::: @@ -516,7 +518,7 @@ Both architectures can *represent* the digit classification function (UAT guaran \index{Architecture!selection as bias matching} The learnability gap motivates the core design principle of this chapter: embed inductive biases that match data structure. Each architecture sacrifices theoretical generality for practical learnability. The No Free Lunch theorem\index{No Free Lunch Theorem} [@wolpert1996lack] formalizes this trade-off: the bias that helps one task may hurt another. CNN's translation invariance aids image classification but hurts tasks where absolute position matters. Architecture selection is fundamentally the act of matching inductive bias to data structure. -These theoretical insights translate directly into engineering decisions. Appropriate inductive biases reduce parameter counts (enabling edge deployment), accelerate convergence (reducing training costs), and produce structured computation patterns that map efficiently to specialized hardware (@sec-hardware-acceleration). A `{python} MLPvsCNN.mlp_params_str`-parameter MLP infeasible for edge deployment becomes a `{python} MLPvsCNN.cnn_params_str`-parameter CNN that fits comfortably, a `{python} MLPvsCNN.param_ratio_str`$\times$ reduction achieved by matching architecture to data structure. With this motivation established, we now examine the specific pattern processing requirements that dense architectures address. +These theoretical insights translate directly into engineering decisions. Appropriate inductive biases reduce parameter counts (enabling edge deployment), accelerate convergence (reducing training costs), and produce structured computation patterns that map efficiently to specialized hardware (@sec-hardware-acceleration). A `{python} MLPvsCNN.mlp_params_str`-parameter MLP infeasible for edge deployment becomes a `{python} MLPvsCNN.cnn_params_str`-parameter CNN that fits comfortably, a `{python} MLPvsCNN.param_ratio_str` $\times$ reduction achieved by matching architecture to data structure. With this motivation established, we now examine the specific pattern processing requirements that dense architectures address. \index{Fully Connected Layer} @@ -696,11 +698,11 @@ Consider a simplified 4-pixel image processed by a 3-neuron hidden layer: **Input**: $\mathbf{h}^{(0)} = [0.8, 0.2, 0.9, 0.1]$ (4 pixel intensities) -**Weight matrix**: $\mathbf{W}^{(1)} = \begin{bmatrix} 0.5 & 0.1 & -0.2 \\ -0.3 & 0.8 & 0.4 \\ 0.2 & -0.4 & 0.6 \\ 0.7 & 0.3 & -0.1 \end{bmatrix}$ (4$\times$3 matrix) +**Weight matrix**: $\mathbf{W}^{(1)} = \begin{bmatrix} 0.5 & 0.1 & -0.2 \\ -0.3 & 0.8 & 0.4 \\ 0.2 & -0.4 & 0.6 \\ 0.7 & 0.3 & -0.1 \end{bmatrix}$ (4 $\times$ 3 matrix) **Computation**: \begin{gather*} -\mathbf{z}^{(1)} = \mathbf{h}^{(0)}\mathbf{W}^{(1)} = \begin{bmatrix} 0.5$\times$0.8 + (-0.3)$\times$0.2 + 0.2$\times$0.9 + 0.7$\times$0.1 \\ 0.1$\times$0.8 + 0.8$\times$0.2 + (-0.4)$\times$0.9 + 0.3$\times$0.1 \\ (-0.2)$\times$0.8 + 0.4$\times$0.2 + 0.6$\times$0.9 + (-0.1)$\times$0.1 \end{bmatrix} +\mathbf{z}^{(1)} = \mathbf{h}^{(0)}\mathbf{W}^{(1)} = \begin{bmatrix} 0.5 $\times$ 0.8 + (-0.3) $\times$ 0.2 + 0.2 $\times$ 0.9 + 0.7 $\times$ 0.1 \\ 0.1 $\times$ 0.8 + 0.8 $\times$ 0.2 + (-0.4) $\times$ 0.9 + 0.3 $\times$ 0.1 \\ (-0.2) $\times$ 0.8 + 0.4 $\times$ 0.2 + 0.6 $\times$ 0.9 + (-0.1) $\times$ 0.1 \end{bmatrix} \\ = \begin{bmatrix} 0.59 \\ -0.09 \\ 0.45 \end{bmatrix} \end{gather*} @@ -936,11 +938,12 @@ Dense connectivity thus moves maximum data for minimum compute. For data with in ## CNNs: Spatial Pattern Processing {#sec-network-architectures-cnns-spatial-pattern-processing-5b8d} -The MLP's assumption that all input features interact equally with all outputs proves particularly costly for spatially structured data like images. As the MNIST comparison above demonstrated, a CNN achieves higher accuracy with 47$\times$ fewer parameters by exploiting spatial locality rather than treating every pixel independently. +The MLP's assumption that all input features interact equally with all outputs proves particularly costly for spatially structured data like images. As the MNIST comparison above demonstrated, a CNN achieves higher accuracy with 47 $\times$ fewer parameters by exploiting spatial locality rather than treating every pixel independently. Convolutional[^fn-convolution-etymology] Neural Networks emerged as the solution to this challenge [@lecun1998gradient; @alexnet2012]. Consider what happens when you look at a photograph: you do not perceive every pixel simultaneously in relation to every other pixel. Instead, you notice local patterns—edges, textures, corners—and compose them into objects. CNNs encode this same insight architecturally. \index{Convolution!etymology} + [^fn-convolution-etymology]: **Convolution**\index{Convolution!definition}: From Latin *convolvere* meaning "to roll together," the mathematical operation appeared in 18th-century work by D'Alembert and Laplace on series expansions. The term "convolution" was first applied to this integral in 1934 by mathematician Aurel Wintner. In signal processing, convolution describes how a system's impulse response "rolls together" with an input signal, a metaphor that extends naturally to the sliding filter operations in neural networks. Spatial locality produces two key innovations that enhance efficiency for spatially structured data. Parameter sharing allows the same feature detector to be applied across different spatial positions, reducing parameters from millions to thousands while improving generalization. Local connectivity restricts connections to spatially adjacent regions, reflecting the insight that spatial proximity correlates with feature relevance. Together, these innovations define *convolutional neural networks* as an architectural family. @@ -962,6 +965,7 @@ This hierarchical processing appears across many domains: local pixel patterns f Focusing on image processing to illustrate these principles, if we want to detect a cat in an image, certain spatial patterns must be recognized: the triangular shape of ears, the round contours of the face, the texture of fur. Importantly, these patterns maintain their meaning regardless of where they appear in the image. A cat is still a cat whether it appears in the top-left or bottom-right corner. This indicates two key requirements for spatial pattern processing: the ability to detect local patterns and the ability to recognize these patterns regardless of their position[^fn-dnn-imagenet]. Look at @fig-cnn-spatial-processing to see how convolutional neural networks meet both requirements through hierarchical feature extraction — observe how simple patterns compose into increasingly complex representations at successive layers. \index{AlexNet!ImageNet revolution} + [^fn-dnn-imagenet]: **ImageNet Revolution**\index{ImageNet}: AlexNet's dramatic victory in the 2012 ImageNet challenge [@alexnet2012] reduced top-5 error from 26.2% (the runner-up) to 15.3%, a 10.9 percentage point improvement that sparked the deep learning renaissance. ImageNet's 14+ million labeled images across more than 21,000 categories (with the ILSVRC competition subset using 1,000 classes) provided the scale needed to train deep CNNs, proving that "big data + big compute + big models" could achieve unprecedented performance. ::: {#fig-cnn-spatial-processing fig-env="figure" fig-pos="htb" fig-cap="**Spatial Feature Extraction**: Convolutional neural networks identify patterns independent of their location in an image by applying learnable filters across the input, enabling robust object recognition. These filters detect local features, and their repeated application across the image creates translation equivariance, preserving spatial relationships between detected patterns regardless of position." fig-alt="Two identical zebra images at different positions in input frames. Arrows show same filter applied to both, producing matching feature activations. Demonstrates translation equivariance: detecting patterns regardless of spatial position in image."} @@ -1252,7 +1256,7 @@ This equation describes how CNNs process spatial data. $\mathbf{H}^{(l)}_{i,j,k} Breaking down the notation further, $(i,j)$ corresponds to spatial positions, $k$ indexes output channels, $c$ indexes input channels, and $(di,dj)$ spans the local receptive field[^fn-receptive-field]. Unlike the dense matrix multiplication of MLPs, this operation: -[^fn-receptive-field]: **Receptive Field**\index{Receptive Field}: The region of the input that influences a particular output neuron. In CNNs, receptive fields grow with depth. A neuron in layer 3 might "see" a 7$\times$7 region even with 3$\times$3 filters, due to stacking. Understanding receptive field size is important for ensuring networks can capture features at the right scale for the task. +[^fn-receptive-field]: **Receptive Field**\index{Receptive Field}: The region of the input that influences a particular output neuron. In CNNs, receptive fields grow with depth. A neuron in layer 3 might "see" a 7 $\times$ 7 region even with 3 $\times$ 3 filters, due to stacking. Understanding receptive field size is important for ensuring networks can capture features at the right scale for the task. Convolutional layers process local neighborhoods (typically $3 \times 3$ or $5 \times 5$), reuse the same weights at each spatial position, and maintain spatial structure in the output. @@ -1388,7 +1392,7 @@ The feature activation shifts by the same amount as the input, demonstrating equ ::: -Equivariance carries systems implications that extend beyond mathematical elegance. Parameter efficiency is the most immediate benefit: equivariance through parameter sharing produces dramatic reductions in model size. Consider processing a 224 $\times$ 224 RGB image. An MLP would require each hidden neuron to connect to all 224 $\times$ 224 $\times$ 3 = `{python} CNNSharing.mlp_img_params_str` input pixels. A CNN with a 3 $\times$ 3 filter needs only 3 $\times$ 3 $\times$ 3 = `{python} CNNSharing.cnn_filter_params_str` parameters per filter, reused across all 224 $\times$ 224 positions. This represents approximately `{python} CNNSharing.cnn_param_reduction_str`$\times$ fewer parameters per feature detector, and the memory savings enable larger models and bigger batches on fixed hardware. +Equivariance carries systems implications that extend beyond mathematical elegance. Parameter efficiency is the most immediate benefit: equivariance through parameter sharing produces dramatic reductions in model size. Consider processing a 224 $\times$ 224 RGB image. An MLP would require each hidden neuron to connect to all 224 $\times$ 224 $\times$ 3 = `{python} CNNSharing.mlp_img_params_str` input pixels. A CNN with a 3 $\times$ 3 filter needs only 3 $\times$ 3 $\times$ 3 = `{python} CNNSharing.cnn_filter_params_str` parameters per filter, reused across all 224 $\times$ 224 positions. This represents approximately `{python} CNNSharing.cnn_param_reduction_str` $\times$ fewer parameters per feature detector, and the memory savings enable larger models and bigger batches on fixed hardware. The computational structure created by equivariance proves equally valuable for systems optimization. The sliding window pattern applies the same operation at every spatial position, creating regular computation that hardware can exploit. Input pixels are used by multiple filter positions, enabling im2col optimizations that restructure data for efficient matrix operations. The resulting computation is inherently SIMD-friendly, as modern GPUs can execute identical instructions across spatial positions simultaneously. This structural regularity explains why TPUs and AI accelerators include specialized units for convolution: the operation maps efficiently to silicon precisely because equivariance creates predictable, parallelizable patterns. @@ -1433,13 +1437,13 @@ These architectural properties make CNNs highly amenable to systems-level analys ::: {.callout-lighthouse title="ResNet-50 (Vision Lighthouse)"} **Why it matters:** ResNet-50 is the gold standard benchmark for **compute-bound** vision workloads. Its architecture consists almost entirely of dense convolutional layers, making it highly regular and efficient on GPUs. Unlike MobileNet (latency-bound) or Transformers (memory-bound), ResNet-50's performance is typically limited by raw floating-point throughput (FLOPs), making it the ideal lighthouse for explaining data parallelism, quantization, and batching strategies. -| **Property** | **Value** | **System Implication** | -|:----------------|:---------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------| -| **Parameters** | `{python} LighthouseSpecs.resnet_params_m_str` million | `{python} LighthouseSpecs.resnet_fp32_mb_str` MB model size at FP32; fits comfortably in GPU memory. | -| **FLOPs/Image** | `{python} LighthouseSpecs.resnet_gflops_str` GFLOPs (224$\times$224) | Dominated by 3$\times$3 convolutions (~90% of compute). | -| **Constraint** | Compute Bound | Limited by raw FLOPs, not memory bandwidth. | -| **Bottleneck** | FP Throughput | Benefits maximally from specialized Matrix Units (Tensor Cores). | -| **Profile** | High Arithmetic Intensity | High ratio of math-to-memory operations (~100 FLOPs/byte). | +| **Property** | **Value** | **System Implication** | +|:----------------|:-----------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------| +| **Parameters** | `{python} LighthouseSpecs.resnet_params_m_str` million | `{python} LighthouseSpecs.resnet_fp32_mb_str` MB model size at FP32; fits comfortably in GPU memory. | +| **FLOPs/Image** | `{python} LighthouseSpecs.resnet_gflops_str` GFLOPs (224 $\times$ 224) | Dominated by 3 $\times$ 3 convolutions (~90% of compute). | +| **Constraint** | Compute Bound | Limited by raw FLOPs, not memory bandwidth. | +| **Bottleneck** | FP Throughput | Benefits maximally from specialized Matrix Units (Tensor Cores). | +| **Profile** | High Arithmetic Intensity | High ratio of math-to-memory operations (~100 FLOPs/byte). | ::: ResNet-50's compute-bound profile assumes abundant hardware resources, but what happens when the deployment target is a smartphone rather than a datacenter GPU? At the opposite end of the efficiency spectrum, MobileNet demonstrates that architectural innovation can achieve similar accuracy with a fraction of the computational cost. @@ -1449,19 +1453,19 @@ ResNet-50's compute-bound profile assumes abundant hardware resources, but what ::: {.callout-lighthouse title="MobileNet (Efficiency Lighthouse)"} **Why it matters:** MobileNet represents **latency-constrained** edge workloads. Its depthwise separable convolutions trade channel mixing capacity for speed, making it the standard baseline for mobile apps, embedded vision, and neural architecture search (NAS). -| **Property** | **Value** | **System Implication** | -|:----------------|:----------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| -| **Parameters** | `{python} LighthouseSpecs.mobilenet_params_m_str` million | `{python} LighthouseSpecs.mobilenet_fp32_mb_str` MB at FP32; `{python} LighthouseSpecs.mobilenet_size_ratio_str`$\times$ smaller than ResNet-50. | -| **FLOPs/Image** | `{python} LighthouseSpecs.mobilenet_mflops_str` MFLOPs | `{python} LighthouseSpecs.mobilenet_flops_ratio_str`$\times$ fewer than ResNet-50 for similar accuracy. | -| **Constraint** | Latency Bound | Single-image inference speed is the priority. | -| **Bottleneck** | Overhead / Serial Ops | Kernel launch overhead often dominates actual compute. | -| **Profile** | Low Arithmetic Intensity | Memory access and control logic matter more than raw FLOPs. | +| **Property** | **Value** | **System Implication** | +|:----------------|:----------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------| +| **Parameters** | `{python} LighthouseSpecs.mobilenet_params_m_str` million | `{python} LighthouseSpecs.mobilenet_fp32_mb_str` MB at FP32; `{python} LighthouseSpecs.mobilenet_size_ratio_str` $\times$ smaller than ResNet-50. | +| **FLOPs/Image** | `{python} LighthouseSpecs.mobilenet_mflops_str` MFLOPs | `{python} LighthouseSpecs.mobilenet_flops_ratio_str` $\times$ fewer than ResNet-50 for similar accuracy. | +| **Constraint** | Latency Bound | Single-image inference speed is the priority. | +| **Bottleneck** | Overhead / Serial Ops | Kernel launch overhead often dominates actual compute. | +| **Profile** | Low Arithmetic Intensity | Memory access and control logic matter more than raw FLOPs. | ::: The contrast between ResNet-50 and MobileNet highlights a counterintuitive lesson that trips up many practitioners. ::: {.callout-warning title="Misconception: FLOPs = Speed"} -**Misconception:** "MobileNet has `{python} LighthouseSpecs.mobilenet_flops_ratio_str`$\times$ fewer FLOPs than ResNet-50, so it must run `{python} LighthouseSpecs.mobilenet_flops_ratio_str`$\times$ faster." +**Misconception:** "MobileNet has `{python} LighthouseSpecs.mobilenet_flops_ratio_str` $\times$ fewer FLOPs than ResNet-50, so it must run `{python} LighthouseSpecs.mobilenet_flops_ratio_str` $\times$ faster." **Reality:** On high-end GPUs, MobileNet often runs *slower* than ResNet-50 despite using far fewer operations. MobileNet's depthwise separable convolutions have low arithmetic intensity: they move more data relative to computation. GPUs optimized for dense matrix operations (high arithmetic intensity) cannot saturate their compute units on MobileNet's memory-bound kernels. FLOPs measure *work*; throughput depends on how well that work *maps to hardware*. This is why MobileNet excels on mobile CPUs (where memory bandwidth matches compute) but underperforms on datacenter GPUs (where compute far exceeds bandwidth). We revisit this hardware-architecture mismatch as a general fallacy in @sec-network-architectures-fallacies-pitfalls-96b4. ::: @@ -1554,7 +1558,7 @@ def conv_layer_spatial(input, kernel, bias): Single call dispatches to optimized kernel (often via im2col + GEMM). """ # Convolution applies shared weights across all positions - # For a 3 × 3 kernel on 28 × 28 input (padded): + # For a 3 $\times$ 3 kernel on 28 $\times$ 28 input (padded): # 9 MACs per position x 784 positions output = convolution(input, kernel) + bias return activation(output) @@ -1613,13 +1617,13 @@ The core computation in CNNs involves repeatedly applying small filters across s This computational pattern presents different optimization opportunities than MLPs. The regular, repeated nature of convolution operations enables efficient hardware utilization through structured parallelism. Modern processors exploit this pattern in various ways. CPUs use SIMD instructions[^fn-simd] to process multiple filter positions simultaneously, while GPUs parallelize computation across spatial positions and channels. The model optimization techniques that further reduce these computational demands, including specialized convolution optimizations and sparsity patterns, are detailed in @sec-model-compression. -[^fn-simd]: **SIMD (Single Instruction, Multiple Data)**\index{SIMD (Single Instruction, Multiple Data)}: CPU instructions that perform the same operation on multiple data elements simultaneously. Modern x86 processors support AVX-512, enabling 16 single-precision operations per instruction, a 16$\times$ speedup over scalar code. SIMD is important for efficient neural network inference on CPUs, especially for edge deployment. Deep learning frameworks further optimize this through specialized convolution algorithms that transform the computation to better match hardware capabilities. +[^fn-simd]: **SIMD (Single Instruction, Multiple Data)**\index{SIMD (Single Instruction, Multiple Data)}: CPU instructions that perform the same operation on multiple data elements simultaneously. Modern x86 processors support AVX-512, enabling 16 single-precision operations per instruction, a 16 $\times$ speedup over scalar code. SIMD is important for efficient neural network inference on CPUs, especially for edge deployment. Deep learning frameworks further optimize this through specialized convolution algorithms that transform the computation to better match hardware capabilities. #### Data Movement {#sec-network-architectures-data-movement-36f6} The sliding window pattern of convolutions creates a distinctive data movement profile. Unlike MLPs where each weight is used once per forward pass, CNN filter weights are reused many times as the filter slides across spatial positions. For ImageNet processing, each $3 \times 3$ filter weight is reused 50,176 times (once for each position in the $224 \times 224$ feature map). This creates a different challenge: the system must stream input features through the computation unit while keeping filter weights stable. -The predictable spatial access pattern enables strategic data movement optimizations. The CPU/GPU caching strategies described above apply directly to data movement: frameworks orchestrate computation to maximize the 50,176$\times$ filter weight reuse and minimize redundant feature map accesses, exploiting the same spatial locality that makes CNNs memory-efficient. +The predictable spatial access pattern enables strategic data movement optimizations. The CPU/GPU caching strategies described above apply directly to data movement: frameworks orchestrate computation to maximize the 50,176 $\times$ filter weight reuse and minimize redundant feature map accesses, exploiting the same spatial locality that makes CNNs memory-efficient. ### Efficient Architectures: Keyword Spotting {#sec-network-architectures-efficient-architectures-keyword-spotting-0625} @@ -1825,10 +1829,10 @@ rnn_macs_step_str = RNNCompute.total_str ::: {#lst-rnn_layer_step lst-cap="**RNN Layer Abstraction**: Framework-level implementation combining two matrix multiplications (h_prev x W_hh and x_t x W_xh) per time step. For 128-dimensional hidden state and 100-dimensional input, each step requires `{python} rnn_recurrent_macs_str` + `{python} rnn_input_macs_str` = `{python} rnn_macs_step_str` MACs, but sequential dependencies prevent parallelization across time."} ```{.python} def rnn_layer_step(x_t, h_prev, W_hh, W_xh, b): - # x_t: input at time t (batch_size × input_dim) - # h_prev: previous hidden state (batch_size × hidden_dim) - # W_hh: recurrent weights (hidden_dim × hidden_dim) - # W_xh: input weights (input_dim × hidden_dim) + # x_t: input at time t (batch_size $\times$ input_dim) + # h_prev: previous hidden state (batch_size $\times$ hidden_dim) + # W_hh: recurrent weights (hidden_dim $\times$ hidden_dim) + # W_xh: input weights (input_dim $\times$ hidden_dim) h_t = activation(matmul(h_prev, W_hh) + matmul(x_t, W_xh) + b) return h_t ``` @@ -1847,12 +1851,12 @@ def rnn_layer_compute(x_t, h_prev, W_hh, W_xh, b): # Loop 1: Process each sequence in the batch for batch in range(batch_size): # Loop 2: Compute recurrent contribution - # (h_prev × W_hh) + # (h_prev $\times$ W_hh) for i in range(hidden_dim): for j in range(hidden_dim): h_t[batch, i] += h_prev[batch, j] * W_hh[j, i] - # Loop 3: Compute input contribution (x_t × W_xh) + # Loop 3: Compute input contribution (x_t $\times$ W_xh) for i in range(hidden_dim): for j in range(input_dim): h_t[batch, i] += x_t[batch, j] * W_xh[j, i] @@ -1989,7 +1993,7 @@ In this equation, $\mathbf{Q}$ (queries), $\mathbf{K}$ (keys), and $\mathbf{V}$ The attention operation involves several key steps. First, it computes query, key, and value projections for each position in the sequence. Next, examine the $N\times N$ attention matrix in @fig-attention — each cell represents a query-key interaction, and the color intensity reveals which positions attend most strongly to which others. Finally, these attention weights combine value vectors to produce the output. -::: {#fig-attention fig-env="figure" fig-pos="htb" fig-cap="**Query-Key-Value Attention Mechanism**: For a 6-token sequence, queries (cyan) match against keys (red) to produce a $6 \times 6$ attention matrix with $O(N^2)$ entries. Color intensity indicates attention weight: darker cells show stronger relationships. Each output position aggregates information from all values (green) weighted by its attention row. The matrix structure reveals both the computational pattern (36 similarity computations) and the memory bottleneck (storing $N^2$ attention weights). Source: Transformer Explainer [@transformer_explainer]." fig-alt="6 × 6 attention matrix with gradient coloring from blue to red indicating attention weights. Cyan query vectors enter from left, red key vectors from top, green value vectors from below. Output vectors exit right, showing weighted aggregation pattern."} +::: {#fig-attention fig-env="figure" fig-pos="htb" fig-cap="**Query-Key-Value Attention Mechanism**: For a 6-token sequence, queries (cyan) match against keys (red) to produce a $6 \times 6$ attention matrix with $O(N^2)$ entries. Color intensity indicates attention weight: darker cells show stronger relationships. Each output position aggregates information from all values (green) weighted by its attention row. The matrix structure reveals both the computational pattern (36 similarity computations) and the memory bottleneck (storing $N^2$ attention weights). Source: Transformer Explainer [@transformer_explainer]." fig-alt="6 $\times$ 6 attention matrix with gradient coloring from blue to red indicating attention weights. Cyan query vectors enter from left, red key vectors from top, green value vectors from below. Output vectors exit right, showing weighted aggregation pattern."} ```{.tikz} \begin{tikzpicture}[line join=round,font=\usefont{T1}{phv}{m}{n}\small] \tikzset{% @@ -2301,7 +2305,7 @@ attn_score_macs_m_str = fmt(attn_score_macs_value / MILLION, precision=1, commas ::: {#lst-attention_layer_compute lst-cap="**Attention Computation**: Two implementations showing the same O(N^2 $\times$ d) complexity. The matrix form (top) uses optimized GEMM, while the nested loops (bottom) expose the quadratic pairwise comparisons: for sequence length 512 and dimension 64, computing attention scores requires 512 $\times$ 512 $\times$ 64 = `{python} attn_score_macs_m_str` million MACs per attention head, plus another `{python} attn_score_macs_m_str`M for value aggregation."} ```{.python} def attention_layer_matrix(Q, K, V): - # Q, K, V: (batch_size × seq_len × d_model) + # Q, K, V: (batch_size $\times$ seq_len $\times$ d_model) scores = matmul(Q, K.transpose(-2, -1)) / sqrt( d_k ) # Compute attention scores @@ -2584,8 +2588,8 @@ The Transformer architecture applies this self-attention mechanism within a broa \node[anchor=north, align=center] at (1.25,-1.5) {Inputs}; \node[anchor=north, align=center] at (5.25,-1.5) {Outputs (shifted right)}; \node[anchor=south, align=center] at (5.25,11.70) {Output Probabilities}; -\node[anchor=east] at (-1.175,3.88) {N$\times$}; -\node[anchor=west] at (7.675,5.355) {N$\times$}; +\node[anchor=east] at (-1.175,3.88) {N $\times$}; +\node[anchor=west] at (7.675,5.355) {N $\times$}; \node[align=center] at (-0.95,0.6) {Positional \\ Encoding}; \node[text width=2cm, anchor=west] at (6.75,0.6) {Positional \vspace{-0.05cm} \linebreak Encoding}; \end{tikzpicture}} @@ -2599,8 +2603,8 @@ While Transformer self-attention builds upon the basic attention mechanism, it i ::: {#lst-self_attention_layer lst-cap="**Self-Attention and Multi-Head Attention**: Self-attention (top) derives Q, K, V from the same input X through three projections, then computes attention as before. Multi-head attention (bottom) runs h parallel attention heads with dimension d_k = d_model/h, then concatenates and projects. For GPT-2 (768-dim, 12 heads), each head operates on 64 dimensions, reducing per-head attention memory while enabling diverse relationship patterns."} ```{.python} def self_attention_layer(X, W_Q, W_K, W_V, d_k): - # X: input tensor (batch_size × seq_len × d_model) - # W_Q, W_K, W_V: weight matrices (d_model × d_k) + # X: input tensor (batch_size $\times$ seq_len $\times$ d_model) + # W_Q, W_K, W_V: weight matrices (d_model $\times$ d_k) Q = matmul(X, W_Q) K = matmul(X, W_K) @@ -2729,6 +2733,7 @@ Unlike the architectures examined so far, which are typically compute-bound or b The core challenge in RecSys is handling high-cardinality categorical features\index{Categorical Features}. A model might need to process User IDs (billions of unique users) and Item IDs (millions of videos or products). We cannot input these raw IDs directly into a neural network; instead, we map each ID to a dense vector called an **embedding**[^fn-embedding-etymology] [@mikolov2013efficient]. \index{Embedding!word2vec origin} + [^fn-embedding-etymology]: **Embedding**\index{Embedding}: From the mathematical concept of embedding one space into another, the term entered ML via word2vec (2013) and related work. Just as a topological embedding maps a space into a higher-dimensional one while preserving structure, neural embeddings map discrete tokens (words, user IDs) into continuous vector spaces where semantic relationships become geometric ones: similar items cluster together, and relationships like "king - man + woman = queen" emerge as vector arithmetic. ### Algorithmic Structure {#sec-network-architectures-algorithmic-structure-732e} @@ -3077,6 +3082,7 @@ Skip connections ensure gradients *reach* early layers; normalization ensures th \index{Ioffe, Sergey} \index{Szegedy, Christian} + [^fn-batchnorm-ioffe]: **Batch Normalization**: Introduced by Sergey Ioffe and Christian Szegedy at Google in 2015. Their insight was that training instability often stems from "internal covariate shift"—the changing distribution of layer inputs during training. By normalizing activations within each mini-batch, they enabled dramatically higher learning rates and reduced sensitivity to initialization, cutting training time for ImageNet models by a factor of 14. The technique became ubiquitous so quickly that within two years, virtually no competitive vision model trained without it. #### Batch Normalization: Definition and Formulation {#sec-network-architectures-batch-normalization-definition-formulation-8579} @@ -3169,6 +3175,7 @@ Gating mechanisms were born in RNNs, where early sequence models hit a "temporal \index{Hochreiter, Sepp} \index{Schmidhuber, Jürgen} + [^fn-lstm-invention]: **LSTM Origins**\index{LSTM (Long Short-Term Memory)}: Sepp Hochreiter and Jürgen Schmidhuber invented LSTMs in 1997 [@hochreiter1997long] to solve the "vanishing gradient problem" that plagued RNNs. Their gating mechanism introduced a "Constant Error Carousel" that protects error signals from decay as they propagate back through time. This breakthrough enabled sequence modeling and facilitated modern language models. [^fn-gru]: **Gated Recurrent Unit (GRU)**\index{GRU (Gated Recurrent Unit)}: Simplified version of LSTM introduced by Cho et al. (2014) [@cho2014properties] with only 2 gates instead of 3, reducing parameters by ~25% while maintaining similar performance. GRUs became popular for their computational efficiency and easier training, proving that architectural simplification can sometimes improve rather than hurt performance. @@ -3275,7 +3282,7 @@ Three operations serve as the irreducible building blocks for all deep learning Matrix multiplication represents the basic form of transforming sets of features. Multiplying a matrix of inputs by a matrix of weights computes weighted combinations -- the core operation of neural networks (recall our reference MLP layer from @sec-network-architectures-algorithmic-structure-99f2). This pattern appears everywhere: MLPs use it directly for layer computations, CNNs reshape convolutions into matrix multiplications, and Transformers use it extensively in their attention mechanisms. To understand why, examine the im2col (image to column) transformation in @fig-im2col-diagram: follow how a convolution over $3\times 3$ input feature maps converts into a matrix operation as each sliding window position unfolds into a column of the transformed matrix. -::: {#fig-im2col-diagram fig-env="figure" fig-pos="htb" fig-cap="**im2col Transformation**: Converts convolution to GEMM by rearranging image patches into columns. The input feature maps (cyan/orange grids, $3 \times 3$) are unfolded so each sliding window position becomes a matrix column, while filter kernels (green/yellow, $2 \times 2$) become rows. The resulting $4 \times 8$ matrix multiplication produces all output positions in one operation. This transformation trades 2$\times$ memory overhead (duplicating overlapping pixels) for 5–10$\times$ speedup by exploiting decades of BLAS optimizations and enabling efficient GPU parallelization." fig-alt="Left: two 3 × 3 input feature maps in cyan and orange. Center: 4 × 8 transformed matrix with unfolded patches as columns. Right: 8 × 1 filter kernel vector. Red boxes highlight how sliding windows become matrix columns for GEMM."} +::: {#fig-im2col-diagram fig-env="figure" fig-pos="htb" fig-cap="**im2col Transformation**: Converts convolution to GEMM by rearranging image patches into columns. The input feature maps (cyan/orange grids, $3 \times 3$) are unfolded so each sliding window position becomes a matrix column, while filter kernels (green/yellow, $2 \times 2$) become rows. The resulting $4 \times 8$ matrix multiplication produces all output positions in one operation. This transformation trades 2 $\times$ memory overhead (duplicating overlapping pixels) for 5–10 $\times$ speedup by exploiting decades of BLAS optimizations and enabling efficient GPU parallelization." fig-alt="Left: two 3 $\times$ 3 input feature maps in cyan and orange. Center: 4 $\times$ 8 transformed matrix with unfolded patches as columns. Right: 8 $\times$ 1 filter kernel vector. Red boxes highlight how sliding windows become matrix columns for GEMM."} ```{.tikz} \begin{tikzpicture}[line join=round,font=\usefont{T1}{phv}{m}{n}\small] \tikzset{ @@ -3388,7 +3395,7 @@ The detailed analysis of sparse computation patterns, including structured and u The im2col[^fn-im2col] (image to column) technique accomplishes matrix reshaping by unfolding overlapping image patches into columns of a matrix (@fig-im2col-diagram). Each sliding window position in the convolution becomes a column in the transformed matrix, while the filter kernels are arranged as rows. This allows the convolution operation to be expressed as a standard GEMM (General Matrix Multiply) operation. -The insight behind this transformation is pragmatic: decades of engineering effort have produced extraordinarily optimized GEMM implementations (cuBLAS, MKL, OpenBLAS), while convolution-specific code would need to be written from scratch. By converting convolutions into matrix multiplications, we inherit all that optimization work for free. The transformation trades memory consumption (duplicating data where windows overlap) for computational efficiency, enabling CNNs to use these mature BLAS libraries and achieving 5–10$\times$ speedups on CPUs. In modern systems, these matrix multiplications map to specific hardware and software implementations. Datacenter accelerators can deliver on the order of hundreds of TFLOPS on mixed-precision matrix operations, and software frameworks like PyTorch and TensorFlow automatically map these high-level operations to optimized matrix libraries (for example, NVIDIA cuBLAS and Intel oneMKL) that exploit available hardware capabilities. +The insight behind this transformation is pragmatic: decades of engineering effort have produced extraordinarily optimized GEMM implementations (cuBLAS, MKL, OpenBLAS), while convolution-specific code would need to be written from scratch. By converting convolutions into matrix multiplications, we inherit all that optimization work for free. The transformation trades memory consumption (duplicating data where windows overlap) for computational efficiency, enabling CNNs to use these mature BLAS libraries and achieving 5–10 $\times$ speedups on CPUs. In modern systems, these matrix multiplications map to specific hardware and software implementations. Datacenter accelerators can deliver on the order of hundreds of TFLOPS on mixed-precision matrix operations, and software frameworks like PyTorch and TensorFlow automatically map these high-level operations to optimized matrix libraries (for example, NVIDIA cuBLAS and Intel oneMKL) that exploit available hardware capabilities. [^fn-im2col]: **im2col (Image to Column)**\index{im2col transformation}: First widely adopted in Caffe (2014), im2col became the default convolution strategy because it avoids writing custom convolution kernels from scratch. Alternative approaches include direct convolution (lower memory overhead, used in some mobile frameworks) and Winograd-based convolution (fewer multiplications but more additions, discussed in @sec-network-architectures-hardware-mapping-optimization-strategies-ec3b). @@ -3408,7 +3415,7 @@ These building blocks explain why certain hardware features exist (tensor cores The efficiency of deep learning models depends heavily on memory access and management. Memory access often constitutes the primary bottleneck in modern ML systems: even a matrix multiplication unit capable of thousands of operations per cycle will remain idle if data is not available in time. Accessing data from DRAM typically requires hundreds of cycles, while on-chip computation requires only a few—a disparity that reveals *the energy cost of data movement* as a first-order design constraint. ::: {.callout-notebook title="The Energy Cost of Data Movement"} -A core systems principle: **moving data costs more than computing on it**. A floating-point multiply-accumulate operation consumes approximately 1–5 pJ depending on process node (e.g., ~4.6 pJ at 45nm [@horowitz2014computing]), while fetching a 32-bit operand from DRAM costs ~26 pJ per access — and full cache-line fetches can reach hundreds of pJ. Even at conservative estimates, this 5–100$\times$ energy ratio explains why memory access patterns, not just FLOP counts, determine real-world efficiency. +A core systems principle: **moving data costs more than computing on it**. A floating-point multiply-accumulate operation consumes approximately 1–5 pJ depending on process node (e.g., ~4.6 pJ at 45nm [@horowitz2014computing]), while fetching a 32-bit operand from DRAM costs ~26 pJ per access — and full cache-line fetches can reach hundreds of pJ. Even at conservative estimates, this 5–100 $\times$ energy ratio explains why memory access patterns, not just FLOP counts, determine real-world efficiency. Revisit the architectures above through this energy lens: MLPs have low data reuse (each weight loaded once per sample) and are therefore energy-dominated by DRAM traffic. CNNs reuse filter weights across spatial positions, amortizing load cost over $H \times W$ applications -- the very locality that makes them compute-bound also makes them energy-efficient. RNNs reuse weights across time steps (high temporal reuse) but pay repeated hidden-state read/write costs at each step. Transformers exhibit the worst case: attention matrices require fresh loads of unique key-value pairs at every position, making long-sequence attention both compute-quadratic and energy-quadratic. These energy profiles directly track the bottleneck column in @tbl-lighthouse-comparison. @@ -3554,11 +3561,11 @@ Dense matrix operations in MLPs achieve excellent arithmetic intensity[^fn-arith [^fn-arithmetic-intensity-dnn]: **Arithmetic Intensity**\index{Arithmetic Intensity!definition}: The ratio of floating-point operations to memory accesses, measured in FLOPs/byte. High arithmetic intensity (>10 FLOPs/byte) enables efficient hardware utilization, while low intensity (<1 FLOPs/byte) makes workloads memory-bound. Attention mechanisms typically have low arithmetic intensity, explaining their energy inefficiency. See @sec-hardware-acceleration for the roofline model that formalizes this metric. -Convolutional operations reduce energy consumption through data reuse but exhibit variable efficiency depending on implementation. Im2col-based convolution implementations trade memory for simplicity, often doubling memory requirements and energy consumption. Direct convolution implementations achieve 3–5$\times$ better energy efficiency by eliminating redundant data movement, particularly for larger kernel sizes. +Convolutional operations reduce energy consumption through data reuse but exhibit variable efficiency depending on implementation. Im2col-based convolution implementations trade memory for simplicity, often doubling memory requirements and energy consumption. Direct convolution implementations achieve 3–5 $\times$ better energy efficiency by eliminating redundant data movement, particularly for larger kernel sizes. Sequential processing in RNNs creates energy efficiency opportunities through temporal data reuse. The constant memory footprint of RNN hidden states allows aggressive caching strategies, reducing DRAM access energy by 80-90% for long sequences. The sequential dependencies limit parallelization opportunities, often resulting in suboptimal hardware utilization and higher energy per operation. -Attention mechanisms in Transformers exhibit the highest energy consumption per operation due to irregular memory access patterns and the need to store attention matrices (the quadratic bottleneck from @sec-network-architectures-system-implications-05a3). Self-attention operations consume 2–3$\times$ more energy per FLOP than standard matrix multiplication, making long-sequence processing energy-prohibitive without architectural modifications such as FlashAttention. +Attention mechanisms in Transformers exhibit the highest energy consumption per operation due to irregular memory access patterns and the need to store attention matrices (the quadratic bottleneck from @sec-network-architectures-system-implications-05a3). Self-attention operations consume 2–3 $\times$ more energy per FLOP than standard matrix multiplication, making long-sequence processing energy-prohibitive without architectural modifications such as FlashAttention. System designers must balance competing trade-offs when supporting different primitives, each with unique characteristics that influence system design and performance. Optimizing for the dense matrix operations common in MLPs and CNNs might come at the cost of flexibility needed for the more dynamic computations in attention mechanisms. Supporting large working sets for Transformers might require sacrificing energy efficiency. @@ -3608,7 +3615,7 @@ Different architectural patterns require distinct optimization strategies for ef Dense matrix operations in MLPs map naturally to tensor processing units and GPU tensor cores (@sec-hardware-acceleration details how these map to specific silicon implementations). These operations benefit from several key optimizations: matrix tiling to fit cache hierarchies, mixed-precision computation to double throughput, and operation fusion to reduce memory traffic. Optimal tile sizes depend on cache hierarchy, typically $64 \times 64$ for L1 cache and $256 \times 256$ for L2, while tensor cores achieve peak efficiency with specific dimension multiples such as $16 \times 16$ blocks for Volta architecture. @sec-ml-frameworks examines how frameworks like PyTorch and JAX translate these high-level operations into optimized kernel launches on specific hardware. -CNNs benefit from specialized convolution algorithms and data layout optimizations that differ significantly from dense matrix operations. Im2col transformations convert convolutions to matrix multiplication but double memory usage. Winograd algorithms[^fn-winograd] reduce arithmetic complexity by 2.25$\times$ for $3 \times 3$ convolutions at the cost of numerical stability. Direct convolution with custom kernels achieves optimal memory efficiency but requires architecture-specific tuning. +CNNs benefit from specialized convolution algorithms and data layout optimizations that differ significantly from dense matrix operations. Im2col transformations convert convolutions to matrix multiplication but double memory usage. Winograd algorithms[^fn-winograd] reduce arithmetic complexity by 2.25 $\times$ for $3 \times 3$ convolutions at the cost of numerical stability. Direct convolution with custom kernels achieves optimal memory efficiency but requires architecture-specific tuning. ```{python} #| label: winograd-calc @@ -3642,13 +3649,13 @@ std_muls_3x3_str = f"{std_muls_3x3_value}" # e.g. "9" winograd_muls_str = f"{winograd_muls_value}" # e.g. "4" ``` -[^fn-winograd]: **Winograd Algorithms**\index{Winograd Algorithm}: Fast convolution algorithms based on Shmuel Winograd's 1980 work on minimal multiplication complexity. For 3 $\times$ 3 convolutions, Winograd reduces multiply operations from `{python} std_muls_3x3_str` to `{python} winograd_muls_str` per output (`{python} winograd_reduction_str`$\times$ reduction) by trading multiplications for additions, which cost less in terms of both latency and energy. Modern deep learning frameworks like cuDNN automatically select Winograd for appropriate layer configurations, though numerical precision degradation at FP16 limits applicability for mixed-precision training. +[^fn-winograd]: **Winograd Algorithms**\index{Winograd Algorithm}: Fast convolution algorithms based on Shmuel Winograd's 1980 work on minimal multiplication complexity. For 3 $\times$ 3 convolutions, Winograd reduces multiply operations from `{python} std_muls_3x3_str` to `{python} winograd_muls_str` per output (`{python} winograd_reduction_str` $\times$ reduction) by trading multiplications for additions, which cost less in terms of both latency and energy. Modern deep learning frameworks like cuDNN automatically select Winograd for appropriate layer configurations, though numerical precision degradation at FP16 limits applicability for mixed-precision training. RNNs require different optimization approaches due to their temporal dependencies. Loop unrolling reduces control overhead but increases memory usage. State vectorization allows SIMD operations across multiple sequences. Wavefront parallelization exploits independence across timesteps for bidirectional processing. Transformer attention demands specialized optimizations that reduce memory usage and complexity. Techniques such as FlashAttention[^fn-flashattention] and sparse attention patterns, which can dramatically reduce resource requirements, are examined in @sec-model-compression. -[^fn-flashattention]: **FlashAttention**\index{FlashAttention}: An IO-aware attention algorithm developed by Tri Dao et al. (2022) that avoids materializing the full N $\times$ N attention matrix in GPU HBM. By fusing attention computation into a single kernel and tiling to fit in SRAM, FlashAttention achieves 2–4$\times$ speedup and reduces memory from O(N^2) to O(N) for sequence length N. This enables training on sequences 4–16$\times$ longer than standard attention implementations, making it foundational for modern long-context language models. +[^fn-flashattention]: **FlashAttention**\index{FlashAttention}: An IO-aware attention algorithm developed by Tri Dao et al. (2022) that avoids materializing the full N $\times$ N attention matrix in GPU HBM. By fusing attention computation into a single kernel and tiling to fit in SRAM, FlashAttention achieves 2–4 $\times$ speedup and reduces memory from O(N^2) to O(N) for sequence length N. This enables training on sequences 4–16 $\times$ longer than standard attention implementations, making it foundational for modern long-context language models. These complexity patterns — detailed in each architecture's System Implications section — define optimal domains for each architecture. MLPs excel when parameter efficiency is not critical, CNNs dominate for moderate-resolution spatial data, RNNs remain viable for very long sequences where memory is constrained, and Transformers excel for complex relational tasks where their computational cost is justified through superior performance. With these quantitative foundations established, we can now formalize the architecture selection process into a systematic decision framework. @@ -3801,11 +3808,11 @@ tc_objdet_headroom_str = fmt(tc_objdet_headroom_value, precision=0, commas=False **The Math**: -1. **Model Cost**: ResNet-50 requires ~`{python} tc_resnet_gflops_str` GFLOPs per 224$\times$224 image. +1. **Model Cost**: ResNet-50 requires ~`{python} tc_resnet_gflops_str` GFLOPs per 224 $\times$ 224 image. 2. **Frame Rate**: `{python} tc_fps_str` FPS required. 3. **Sustained Throughput**: `{python} tc_fps_str` $\times$ `{python} tc_resnet_gflops_str` GFLOPs = **`{python} tc_sustained_gflops_str` GFLOPs/sec**. -**The Systems Conclusion**: A mid-range GPU delivering `{python} tc_gpu_tflops_str` TFLOPS theoretical peak achieves ~50--60% utilization in practice, yielding **`{python} tc_effective_low_str`--`{python} tc_effective_high_str` TFLOPS effective**. For ResNet-50 at `{python} tc_fps_str` FPS, you have **`{python} tc_headroom_str`$\times$ headroom**, easily achievable. But switch to an object detection model at `{python} tc_objdet_gflops_str` GFLOPs per frame, and you need **`{python} tc_objdet_sustained_str` TFLOPS sustained**, leaving only **`{python} tc_objdet_headroom_str`$\times$ headroom**. Add batch size constraints or multi-stream processing, and you quickly approach the compute ceiling. ResNet-50 is **Compute-Bound**, but with comfortable margins on modern hardware. +**The Systems Conclusion**: A mid-range GPU delivering `{python} tc_gpu_tflops_str` TFLOPS theoretical peak achieves ~50--60% utilization in practice, yielding **`{python} tc_effective_low_str`--`{python} tc_effective_high_str` TFLOPS effective**. For ResNet-50 at `{python} tc_fps_str` FPS, you have **`{python} tc_headroom_str` $\times$ headroom**, easily achievable. But switch to an object detection model at `{python} tc_objdet_gflops_str` GFLOPs per frame, and you need **`{python} tc_objdet_sustained_str` TFLOPS sustained**, leaving only **`{python} tc_objdet_headroom_str` $\times$ headroom**. Add batch size constraints or multi-stream processing, and you quickly approach the compute ceiling. ResNet-50 is **Compute-Bound**, but with comfortable margins on modern hardware. ::: ```{python} @@ -3882,7 +3889,7 @@ With the throughput ceiling established, we can now apply the complete decision #### Step 1: Data Characterization {.unnumbered} -The input is spatial data (images from camera traps, typically 1920$\times$1080 resolution, downsampled to 224$\times$224 for processing). The task requires recognizing visual patterns (fur textures, body shapes, distinctive markings) that are: +The input is spatial data (images from camera traps, typically 1920 $\times$ 1080 resolution, downsampled to 224 $\times$ 224 for processing). The task requires recognizing visual patterns (fur textures, body shapes, distinctive markings) that are: - **Spatially local**: Species identification relies on local features (ear shape, stripe patterns) - **Translation invariant**: A deer in the top-left is still a deer in the bottom-right @@ -3911,7 +3918,7 @@ Compare candidates against our Lighthouse models: **Architecture Selection:** MobileNetV2 variant with width multiplier 0.75 - **Parameters:** ~`{python} mnv2_params_str` M (~`{python} mnv2_fp32_str` MB FP32, ~`{python} mnv2_int8_str` MB INT8) -- **FLOPs:** ~`{python} mnv2_flops_str` MFLOPs at 224$\times$224 +- **FLOPs:** ~`{python} mnv2_flops_str` MFLOPs at 224 $\times$ 224 - **Rationale:** Sufficient capacity for 50-class problem; fits memory budget with margin; depthwise separable convolutions are power-efficient #### Step 4: Systems Validation {.unnumbered} @@ -3937,7 +3944,7 @@ At 100 inferences/day: `{python} energy_j_str` J/day → negligible vs. sleep po | **Thermal throttling in enclosure** | Add passive heatsink; reduce inference frequency in high-temperature conditions | | **New species added post-deployment** | Reserve 10% model capacity; plan for OTA update mechanism | -**Final Decision:** MobileNetV2 (0.75$\times$ width) with INT8 quantization, deployed on Cortex-A53 SoC with 512 MB RAM. +**Final Decision:** MobileNetV2 (0.75 $\times$ width) with INT8 quantization, deployed on Cortex-A53 SoC with 512 MB RAM. This architecture achieves the accuracy target while operating within the 2W power envelope, processing images in <100 ms, and leaving sufficient memory headroom for system operations. The decision was driven by matching the CNN inductive bias to spatial data characteristics, then validating against hardware constraints using quantitative analysis. ::: @@ -3950,19 +3957,19 @@ This worked example demonstrates the systematic approach that transforms archite **Fallacy:** *More complex architectures always perform better than simpler ones.* -Engineers often assume that Transformers outperform simpler architectures on all tasks. In production, architectural sophistication must match problem complexity. As demonstrated in @sec-network-architectures-architectural-principles-engineering-tradeoffs-362e, a CNN achieves 99% accuracy on MNIST with `{python} MLPvsCNN.cnn_params_str` parameters while an MLP requires `{python} MLPvsCNN.mlp_params_str` parameters for 98% accuracy—a `{python} MLPvsCNN.param_ratio_str`$\times$ parameter reduction with higher accuracy. For problems with spatial locality, CNNs exploit inductive biases that MLPs cannot match. Teams defaulting to Transformers for tabular data or small-image classification waste 5–10$\times$ resources. A \$1,000 training job becomes \$10,000 with no accuracy benefit. +Engineers often assume that Transformers outperform simpler architectures on all tasks. In production, architectural sophistication must match problem complexity. As demonstrated in @sec-network-architectures-architectural-principles-engineering-tradeoffs-362e, a CNN achieves 99% accuracy on MNIST with `{python} MLPvsCNN.cnn_params_str` parameters while an MLP requires `{python} MLPvsCNN.mlp_params_str` parameters for 98% accuracy—a `{python} MLPvsCNN.param_ratio_str` $\times$ parameter reduction with higher accuracy. For problems with spatial locality, CNNs exploit inductive biases that MLPs cannot match. Teams defaulting to Transformers for tabular data or small-image classification waste 5–10 $\times$ resources. A \$1,000 training job becomes \$10,000 with no accuracy benefit. **Pitfall:** *Selecting architectures based solely on accuracy metrics without analyzing computational requirements.* -Practitioners choose architectures from papers reporting state-of-the-art accuracy, ignoring computational implications. As shown in @sec-network-architectures-computational-complexity-considerations-08da, RNNs achieve only 30–50% of peak hardware performance versus 80–90% for MLPs due to sequential constraints. Transformers face the quadratic memory scaling detailed in @sec-network-architectures-system-implications-77ac: sequence length 2048 requires 16$\times$ more memory than length 512 (since $2048^2 / 512^2 = 16$). Production systems ignoring these characteristics miss latency SLAs (100 ms target becomes 500 ms), exceed memory budgets (8 GB becomes 32 GB), or achieve 25% hardware efficiency instead of the expected 80%. These mismatches add 2–6 months to deployment timelines. +Practitioners choose architectures from papers reporting state-of-the-art accuracy, ignoring computational implications. As shown in @sec-network-architectures-computational-complexity-considerations-08da, RNNs achieve only 30–50% of peak hardware performance versus 80–90% for MLPs due to sequential constraints. Transformers face the quadratic memory scaling detailed in @sec-network-architectures-system-implications-77ac: sequence length 2048 requires 16 $\times$ more memory than length 512 (since $2048^2 / 512^2 = 16$). Production systems ignoring these characteristics miss latency SLAs (100 ms target becomes 500 ms), exceed memory budgets (8 GB becomes 32 GB), or achieve 25% hardware efficiency instead of the expected 80%. These mismatches add 2–6 months to deployment timelines. **Fallacy:** *Architecture performance transfers uniformly across different hardware platforms.* -Engineers assume GPU benchmarks predict edge device performance. In reality, hardware-architecture alignment determines efficiency. As discussed in @sec-network-architectures-systemlevel-building-blocks-41c5, CNNs achieve 80–95% of peak throughput on matrix acceleration units, while RNNs' irregular memory access yields only 30–50%. A Transformer running at 50 ms on an A100 may require 2000 ms on a mobile SoC—a 40$\times$ slowdown due to lack of high-bandwidth memory and tensor cores. This gap renders the model unusable for interactive applications requiring sub-200 ms response. Organizations benchmarking only on training hardware discover these gaps late, forcing architecture redesigns that delay launches by quarters. +Engineers assume GPU benchmarks predict edge device performance. In reality, hardware-architecture alignment determines efficiency. As discussed in @sec-network-architectures-systemlevel-building-blocks-41c5, CNNs achieve 80–95% of peak throughput on matrix acceleration units, while RNNs' irregular memory access yields only 30–50%. A Transformer running at 50 ms on an A100 may require 2000 ms on a mobile SoC—a 40 $\times$ slowdown due to lack of high-bandwidth memory and tensor cores. This gap renders the model unusable for interactive applications requiring sub-200 ms response. Organizations benchmarking only on training hardware discover these gaps late, forcing architecture redesigns that delay launches by quarters. **Pitfall:** *Combining architectural patterns without analyzing interaction effects at the system level.* -Engineers add attention to CNNs or convolutions to Transformers expecting additive benefits. Each pattern creates distinct memory access characteristics: CNNs exploit spatial locality through sliding windows, while attention requires all-to-all communication. Naive combinations create bandwidth conflicts—attention layers flush CNN feature maps from cache, eliminating locality benefits. A ResNet achieving 250 images/second can drop to 80 images/second when attention disrupts the cache-optimized pipeline, a 3$\times$ throughput reduction requiring tripled infrastructure to maintain capacity. Adding recurrent connections to Transformers reintroduces sequential dependencies that eliminate parallelization advantages. Successful hybrids require profiling memory access and cache behavior before combining patterns. +Engineers add attention to CNNs or convolutions to Transformers expecting additive benefits. Each pattern creates distinct memory access characteristics: CNNs exploit spatial locality through sliding windows, while attention requires all-to-all communication. Naive combinations create bandwidth conflicts—attention layers flush CNN feature maps from cache, eliminating locality benefits. A ResNet achieving 250 images/second can drop to 80 images/second when attention disrupts the cache-optimized pipeline, a 3 $\times$ throughput reduction requiring tripled infrastructure to maintain capacity. Adding recurrent connections to Transformers reintroduces sequential dependencies that eliminate parallelization advantages. Successful hybrids require profiling memory access and cache behavior before combining patterns. ```{python} #| label: a100-cluster-memory @@ -3992,7 +3999,7 @@ a100_8x_mem_str = f"{a100_8x_mem_value}" # e.g. "640" **Pitfall:** *Optimizing architectural decisions for training hardware without considering deployment constraints.* -Teams design for high-end GPU clusters, then discover deployment failures on target hardware. An architecture exploiting 8$\times$ A100 GPUs (`{python} a100_8x_mem_str` GB total memory) cannot deploy to edge devices with 4 GB—the 160$\times$ reduction requires architectural changes, not just quantization. As @sec-network-architectures-decision-framework-a889 emphasizes, architecture selection must analyze the full system stack. Edge deployment compounds constraints: models must fit 10–100 MB storage, execute in 50–200 ms, and operate within 2–5 W power. Organizations deferring deployment considerations to "optimize later" encounter mismatches requiring costly redesigns that delay products by months. +Teams design for high-end GPU clusters, then discover deployment failures on target hardware. An architecture exploiting 8 $\times$ A100 GPUs (`{python} a100_8x_mem_str` GB total memory) cannot deploy to edge devices with 4 GB—the 160 $\times$ reduction requires architectural changes, not just quantization. As @sec-network-architectures-decision-framework-a889 emphasizes, architecture selection must analyze the full system stack. Edge deployment compounds constraints: models must fit 10–100 MB storage, execute in 50–200 ms, and operate within 2–5 W power. Organizations deferring deployment considerations to "optimize later" encounter mismatches requiring costly redesigns that delay products by months. With these cautionary notes in mind, we now synthesize the key concepts that practitioners should carry forward from this chapter's systematic tour of architectural families, shared building blocks, computational primitives, and selection methodology. @@ -4009,7 +4016,7 @@ The five Lighthouse Models established at the chapter opening (ResNet-50, GPT-2, * **Quadratic costs are permanent constraints**: Transformer attention scales as $O(n^2)$ in memory with sequence length. This is not an implementation detail to optimize away—it is a fundamental property that constrains deployment contexts. * **Lighthouse models isolate distinct bottlenecks**: ResNet-50 (compute), GPT-2 (bandwidth), DLRM (capacity), MobileNet (latency), KWS (power). Use these archetypes to diagnose which physical constraint dominates your system. * **Depth requires architectural support**: Skip connections and normalization layers are not optimizations but *prerequisites* for training networks beyond ~20 layers. These building blocks—born in CNNs—transfer to every modern architecture, including Transformers. -* **FLOPs do not equal speed**: MobileNet uses 14$\times$ fewer FLOPs than ResNet-50 but can run *slower* on datacenter GPUs because its low arithmetic intensity starves compute units. Architecture-hardware alignment, not operation count, determines throughput. +* **FLOPs do not equal speed**: MobileNet uses 14 $\times$ fewer FLOPs than ResNet-50 but can run *slower* on datacenter GPUs because its low arithmetic intensity starves compute units. Architecture-hardware alignment, not operation count, determines throughput. * **Architecture selection is deployment selection**: Choosing a Transformer over a CNN is not just an accuracy decision—it determines memory requirements, latency floors, hardware utilization, and infrastructure costs. The architecture *is* the system constraint. ::: diff --git a/book/quarto/contents/vol1/nn_computation/nn_computation.qmd b/book/quarto/contents/vol1/nn_computation/nn_computation.qmd index 1e75f7a3ea..1e3eff0c93 100644 --- a/book/quarto/contents/vol1/nn_computation/nn_computation.qmd +++ b/book/quarto/contents/vol1/nn_computation/nn_computation.qmd @@ -20,7 +20,7 @@ engine: jupyter ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{20}{15}{90}{55}{10}{0}{0}{15} +\mlsysstack{20}{15}{90}{55}{10}{0}{0}{10} \end{marginfigure} _Why does understanding a neural network's math matter more than reading its code?_ @@ -271,9 +271,9 @@ Line/.style={line width=1.0pt,black!50,text=black}, ``` ::: -Despite their apparent simplicity, rule-based limitations surface quickly with complex real-world tasks. Recognizing human activities illustrates the challenge. Classifying movement below 4 mph as walking seems straightforward until real-world complexity intrudes. Speed variations, transitions between activities, and boundary cases each demand additional rules, creating unwieldy decision trees (@fig-activity-rules). Computer vision tasks compound these difficulties. Detecting cats requires rules about ears, whiskers, and body shapes while accounting for viewing angles, lighting, occlusions, and natural variations. Early systems achieved success only in controlled environments with well-defined constraints. +Despite their apparent simplicity, rule-based limitations surface quickly with complex real-world tasks. Recognizing human activities illustrates the challenge. Classifying movement below 6 km/h as walking seems straightforward until real-world complexity intrudes. Speed variations, transitions between activities, and boundary cases each demand additional rules, creating unwieldy decision trees (@fig-activity-rules). Computer vision tasks compound these difficulties. Detecting cats requires rules about ears, whiskers, and body shapes while accounting for viewing angles, lighting, occlusions, and natural variations. Early systems achieved success only in controlled environments with well-defined constraints. -![**Activity Classification Decision Tree**: A rule-based decision tree classifies human activity by branching on speed thresholds, with values below 4 mph mapped to walking, 4 to 15 mph to running, and above 15 mph to biking. Real-world edge cases and transitions between activities demand increasingly complex branching logic.](images/png/activities.png){#fig-activity-rules fig-alt="Decision tree flowchart for activity classification. Branches split on conditions like speed less than 4 mph leading to walking, 4-15 mph to running, greater than 15 mph to biking. Additional branches handle edge cases and transitions."} +![**Activity Classification Decision Tree**: A rule-based decision tree classifies human activity by branching on speed thresholds, with values below 6 km/h mapped to walking, 6 to 20 km/h to running, and above 20 km/h to biking. Real-world edge cases and transitions between activities demand increasingly complex branching logic.](images/png/activities.png){#fig-activity-rules fig-alt="Decision tree flowchart for activity classification. Branches split on conditions like speed less than 6 km/h leading to walking, 6-20 km/h to running, greater than 20 km/h to biking. Additional branches handle edge cases and transitions."} [^fn-breakout-game]: **Breakout**: The classic 1976 arcade game by Atari became historically significant in AI when DeepMind's DQN (Deep Q-Network) learned to play it from pixels alone, achieving superhuman performance without any programmed game rules [@mnih2015humanlevel]. This breakthrough demonstrated that neural networks could learn complex strategies purely from raw sensory input and reward signals, marking a key milestone in deep reinforcement learning that influences modern AI game-playing systems. @@ -294,7 +294,7 @@ This approach introduced feature engineering by transforming raw data into repre [^fn-feature-engineering]: **Feature Engineering**\index{Feature Engineering!etymology}: From Latin *factura* (a making) and Old French *engigneor* (one who designs). The manual process of transforming raw data into representations that expose patterns to learning algorithms, feature engineering dominated ML practice from the 1990s through 2012. Each new domain required years of expert experimentation. Deep learning eliminated this bottleneck by learning features directly from data, shifting the challenge from *feature design* to *architecture design*. See @sec-network-architectures. -[^fn-hog-method]: **Histogram of Oriented Gradients (HOG)**: Developed by Navneet Dalal and Bill Triggs in 2005, HOG became the gold standard for object detection before deep learning. It achieved near-perfect accuracy on pedestrian detection—a breakthrough that enabled practical computer vision applications. HOG works by computing gradients (edge directions) in 8$\times$8 pixel cells, then creating histograms of 9 orientation bins. This clever abstraction captures object shape while ignoring texture details, making it robust to lighting changes but requiring expert knowledge to design. +[^fn-hog-method]: **Histogram of Oriented Gradients (HOG)**: Developed by Navneet Dalal and Bill Triggs in 2005, HOG became the gold standard for object detection before deep learning. It achieved near-perfect accuracy on pedestrian detection—a breakthrough that enabled practical computer vision applications. HOG works by computing gradients (edge directions) in 8 $\times$ 8 pixel cells, then creating histograms of 9 orientation bins. This clever abstraction captures object shape while ignoring texture details, making it robust to lighting changes but requiring expert knowledge to design. Complementary methods like SIFT [@lowe1999object][^fn-sift] (Scale-Invariant Feature Transform) and Gabor filters[^fn-gabor-filters] captured different visual patterns. SIFT detected keypoints stable across scale and orientation changes, while Gabor filters identified textures and frequencies. Each encoded domain expertise about visual pattern recognition. @@ -304,7 +304,7 @@ Complementary methods like SIFT [@lowe1999object][^fn-sift] (Scale-Invariant Fea These engineering efforts enabled advances in computer vision during the 2000s. Systems could now recognize objects with some robustness to real-world variations, leading to applications in face detection, pedestrian detection, and object recognition. Despite these successes, the approach had limitations. Experts needed to carefully design feature extractors for each new problem, and the resulting features might miss important patterns that were not anticipated in their design. The bottleneck remained: human expertise could not scale to the complexity and diversity of real-world visual patterns. -Return to the same 28 $\times$ 28 digit. HOG divides the image into a `{python} hog_grid_str` $\times$ `{python} hog_grid_str` grid of 4 $\times$ 4 cells, computes gradient magnitudes and orientations at each pixel, bins them into `{python} hog_bins_str` orientation histograms per cell, and produces a `{python} hog_features_str`-element feature vector. A linear classifier (SVM) then performs 10 dot products over that vector. Total: roughly `{python} hog_ops_approx_str` arithmetic operations and ~`{python} hog_mem_str` KB of working memory—about 80$\times$ more compute than the rule-based approach, but still structured, predictable, and well-served by CPU vector units (SIMD). Resource demands scale linearly with image count, not with model complexity. +Return to the same 28 $\times$ 28 digit. HOG divides the image into a `{python} hog_grid_str` $\times$ `{python} hog_grid_str` grid of 4 $\times$ 4 cells, computes gradient magnitudes and orientations at each pixel, bins them into `{python} hog_bins_str` orientation histograms per cell, and produces a `{python} hog_features_str`-element feature vector. A linear classifier (SVM) then performs 10 dot products over that vector. Total: roughly `{python} hog_ops_approx_str` arithmetic operations and ~`{python} hog_mem_str` KB of working memory—about 80 $\times$ more compute than the rule-based approach, but still structured, predictable, and well-served by CPU vector units (SIMD). Resource demands scale linearly with image count, not with model complexity. ### Automatic Pattern Discovery {#sec-neural-computation-deep-learning-automatic-pattern-discovery-214d} @@ -423,7 +423,7 @@ Neural network performance often follows empirical scaling relationships that im This approach reshapes AI system construction. Learning directly from raw data eliminates manual feature engineering but introduces new demands: infrastructure to handle massive datasets, powerful hardware to process that data, and specialized accelerators to perform mathematical calculations efficiently. These computational requirements have driven the development of specialized chips optimized for neural network operations. Empirical evidence confirms this pattern across domains: the success of deep learning in computer vision, speech recognition, game playing, and natural language understanding has established it as the dominant paradigm in artificial intelligence. -Return to the same 28 $\times$ 28 digit, now processed by even a modest three-layer neural network (784→128→64→10). The forward pass alone requires `{python} dl_total_macs_str` multiply-accumulate (MAC) operations—`{python} dl_ops_ratio_str`$\times$ more than the rule-based approach. The `{python} dl_params_str` learned parameters consume ~`{python} dl_weight_kb_str` KB in FP32, exceeding most L1 caches and forcing memory traffic between cache levels on every inference. Training multiplies the cost further: each image must be processed *forward*, then *backward* (computing gradients for all `{python} dl_params_str` parameters), then updated—roughly 3$\times$ the forward cost—repeated over 60,000 images for multiple epochs. The computation is no longer sequential; it is dominated by dense matrix multiplications that leave a standard CPU mostly idle. This is the systems explosion that drives everything that follows. +Return to the same 28 $\times$ 28 digit, now processed by even a modest three-layer neural network (784→128→64→10). The forward pass alone requires `{python} dl_total_macs_str` multiply-accumulate (MAC) operations—`{python} dl_ops_ratio_str` $\times$ more than the rule-based approach. The `{python} dl_params_str` learned parameters consume ~`{python} dl_weight_kb_str` KB in FP32, exceeding most L1 caches and forcing memory traffic between cache levels on every inference. Training multiplies the cost further: each image must be processed *forward*, then *backward* (computing gradients for all `{python} dl_params_str` parameters), then updated—roughly 3 $\times$ the forward cost—repeated over 60,000 images for multiple epochs. The computation is no longer sequential; it is dominated by dense matrix multiplications that leave a standard CPU mostly idle. This is the systems explosion that drives everything that follows. This scaling advantage comes with computational costs that raise a practical question: when should engineers invest in neural networks versus simpler alternatives? @@ -438,18 +438,18 @@ Not every problem benefits from deep learning. Before investing in neural networ | **Dataset size** | > 10,000 labeled examples | Below this, simpler models often match or exceed NN performance | | **Input dimensionality** | > 100 raw features | NNs excel at automatic feature learning from high-dimensional data | | **Data has structure** | Spatial, sequential, or hierarchical patterns | Architecture can encode inductive bias | -| **Accuracy requirement** | Need > 5% improvement over baseline | Each +1% typically costs ~10$\times$ compute | +| **Accuracy requirement** | Need > 5% improvement over baseline | Each +1% typically costs ~10 $\times$ compute | | **Problem complexity** | Non-linear relationships dominate | Linear models handle linear relationships more efficiently | **Use Simpler Methods When:** -| **Condition** | **Better Alternative** | **Typical Outcome** | -|:----------------------------------|:--------------------------------------|:--------------------------------------------------------| -| **< 1,000 samples** | Logistic regression, Random Forest | 10 ms training vs. hours; similar accuracy | -| **Tabular data, < 100 features** | Gradient Boosting (XGBoost, LightGBM) | Often matches NN accuracy with 100$\times$ less compute | -| **Linear relationships** | Linear/Ridge regression | Interpretable, fast, often better generalization | -| **Real-time constraint < 0.1 ms** | Rule-based system | Deterministic latency, no model loading overhead | -| **Explainability required** | Decision trees, linear models | Regulatory compliance, debugging clarity | +| **Condition** | **Better Alternative** | **Typical Outcome** | +|:----------------------------------|:--------------------------------------|:---------------------------------------------------------| +| **< 1,000 samples** | Logistic regression, Random Forest | 10 ms training vs. hours; similar accuracy | +| **Tabular data, < 100 features** | Gradient Boosting (XGBoost, LightGBM) | Often matches NN accuracy with 100 $\times$ less compute | +| **Linear relationships** | Linear/Ridge regression | Interpretable, fast, often better generalization | +| **Real-time constraint < 0.1 ms** | Rule-based system | Deterministic latency, no model loading overhead | +| **Explainability required** | Decision trees, linear models | Regulatory compliance, debugging clarity | **The Baseline Test**: Before building a neural network, train a logistic regression or gradient boosting model in < 1 hour. If it achieves > 90% of your target accuracy, the neural network's additional complexity may not be justified. The USPS system (@sec-neural-computation-case-study-usps-digit-recognition-97be) succeeded partly because the problem genuinely required hierarchical feature learning that simpler methods could not provide. @@ -457,7 +457,7 @@ Not every problem benefits from deep learning. Before investing in neural networ ### Computational Infrastructure Requirements {#sec-neural-computation-computational-infrastructure-requirements-b5b0} -The MNIST running example traced a single digit from ~`{python} rb_ops_str` comparisons (rule-based) through ~`{python} hog_ops_approx_str` structured operations (HOG) to `{python} dl_total_macs_str` matrix MACs (neural network)—a `{python} dl_ops_ratio_str`$\times$ escalation in computation, with a corresponding shift from predictable sequential access to bandwidth-hungry parallel matrix operations. @tbl-evolution generalizes this pattern across every systems dimension. +The MNIST running example traced a single digit from ~`{python} rb_ops_str` comparisons (rule-based) through ~`{python} hog_ops_approx_str` structured operations (HOG) to `{python} dl_total_macs_str` matrix MACs (neural network)—a `{python} dl_ops_ratio_str` $\times$ escalation in computation, with a corresponding shift from predictable sequential access to bandwidth-hungry parallel matrix operations. @tbl-evolution generalizes this pattern across every systems dimension. | **System Aspect** | **Traditional Programming** | **ML with Features** | **Deep Learning** | |:---------------------|:------------------------------|:----------------------------|:--------------------------------| @@ -474,7 +474,7 @@ The computational paradigm shift becomes apparent when comparing these approache \index{Memory Wall!data movement bottleneck} This shift toward parallelism creates new bottlenecks that differ qualitatively from those in sequential computing. The central challenge is the **memory wall**[^fn-memory-hierarchy]: while computational capacity can be increased by adding more processing units, memory bandwidth to feed those units does not scale as favorably. Matrix multiplication, the core neural network operation, is often limited by memory bandwidth rather than raw computational capability[^fn-memory-bound]—simply adding more processing units does not proportionally improve performance. Hardware architectures that address this challenge are examined in @sec-hardware-acceleration, the complete derivation of training memory costs (weights, gradients, optimizer state, activations) appears in @sec-algorithm-foundations, and the formal memory hierarchy with quantitative latency comparisons is in @sec-machine-foundations. -[^fn-memory-hierarchy]: **Memory Hierarchy Performance**: Processors use multiple memory levels (L1 cache, L2 cache, main memory) with vastly different speeds, creating a 50–100$\times$ difference between fastest cache and main memory. This hierarchy shapes neural network accelerator design, which is explored in @sec-hardware-acceleration. +[^fn-memory-hierarchy]: **Memory Hierarchy Performance**: Processors use multiple memory levels (L1 cache, L2 cache, main memory) with vastly different speeds, creating a 50–100 $\times$ difference between fastest cache and main memory. This hierarchy shapes neural network accelerator design, which is explored in @sec-hardware-acceleration. [^fn-memory-bound]: **Memory-Bound Operations**: A workload where the processor spends more time waiting for data than performing calculations. As detailed in @sec-hardware-acceleration, this occurs when the operation's arithmetic intensity falls below the hardware's ridge point. Neural networks are often memory-bound because moving weights from storage to compute units consumes more time and energy than the matrix multiplication itself. @@ -810,18 +810,18 @@ gpt4_params_t_str = HistoricalScale.gpt4_params_t_str |:---------|:-----------|-------------------------------------------:|--------------------:|--------------------------:|:---------------|:-----------------| | 1989 | LeNet-1 | ~9.8K | $10^{11}$–$10^{12}$ | Sun-4/260 workstation | 3 days | 1.0% (USPS) | | 1998 | LeNet-5 | 60K ±1K | $10^{14}$ ±1 OoM | SGI Origin 2000 (200 MHz) | 2–3 days | 0.95% (MNIST) | -| 2012 | AlexNet | ~60M | $5 \times 10^{17}$ | 2$\times$ GTX 580 GPUs | 5–6 days | 15.3% (ImageNet) | -| 2015 | ResNet-152 | ~60M | $10^{19}$ ±0.5 OoM | 8$\times$ Tesla K80 GPUs | ~3 weeks | 3.6% (ImageNet) | +| 2012 | AlexNet | ~60M | $5 \times 10^{17}$ | 2 $\times$ GTX 580 GPUs | 5–6 days | 15.3% (ImageNet) | +| 2015 | ResNet-152 | ~60M | $10^{19}$ ±0.5 OoM | 8 $\times$ Tesla K80 GPUs | ~3 weeks | 3.6% (ImageNet) | | 2020 | GPT-3 | `{python} gpt3_params_b_str`B (exact) | $3 \times 10^{23}$ | ~10K V100 GPUs | weeks | N/A (language) | | 2023 | GPT-4 | ~`{python} gpt4_params_t_str`T (MoE, est.) | $10^{24}$–$10^{25}$ | 10-25K A100s (est.) | months | N/A (language) | -: **Historical Performance**: Four decades of neural network evolution showing the co-scaling of model parameters, training compute, and hardware infrastructure. Training FLOPs increased by approximately $10^{13}$$\times$ from LeNet-1 to GPT-4, while parameters grew by $10^{8}$$\times$. **Uncertainty notes**: Earlier systems (LeNet, AlexNet) have well-documented specifications; recent closed models (GPT-4) have only external estimates (OpenAI has not officially confirmed GPT-4's architecture or parameter count; the ~1.8T MoE estimate is based on public reporting and analysis). "OoM" = order of magnitude uncertainty. {#tbl-historical-performance} +: **Historical Performance**: Four decades of neural network evolution showing the co-scaling of model parameters, training compute, and hardware infrastructure. Training FLOPs increased by approximately $10^{13}$ $\times$ from LeNet-1 to GPT-4, while parameters grew by $10^{8}$ $\times$. **Uncertainty notes**: Earlier systems (LeNet, AlexNet) have well-documented specifications; recent closed models (GPT-4) have only external estimates (OpenAI has not officially confirmed GPT-4's architecture or parameter count; the ~1.8T MoE estimate is based on public reporting and analysis). "OoM" = order of magnitude uncertainty. {#tbl-historical-performance} Beyond raw compute, this exponential growth carries an energy cost that systems engineers cannot ignore. Training LeNet-1 in 1989 consumed roughly 50 kWh—about two days of household electricity. Training GPT-4 consumed an estimated 50,000 MWh, enough to power approximately 5,000 US homes for a year.[^fn-us-energy] The energy cost of AI has moved from negligible to industrial, forcing engineers to treat energy efficiency (Joules per operation) as a primary design constraint alongside raw FLOPS. The quantitative energy analysis, including Horowitz's data-movement-dominates-compute numbers and the full energy hierarchy, appears in @sec-hardware-acceleration where it can be connected to concrete hardware architectures. [^fn-us-energy]: US Energy Information Administration, "Annual Energy Review." The average US household consumes roughly 10.5 MWh of electricity per year. -Three quantitative patterns emerge from this historical data. Training compute for frontier AI models grows approximately 4–5$\times$ per year, with a doubling time of roughly 5–6 months—about 4$\times$ faster than Moore's Law for transistor density. Separately, the compute required to achieve a fixed benchmark halves approximately every 8 months for language models due to algorithmic improvements alone. And while compute grows at 4–5$\times$ per year, training *costs* grow only at roughly 2.4$\times$ per year, reflecting efficiency gains in hardware utilization and reduced precision arithmetic—though frontier model training costs have still risen from thousands of dollars (LeNet era) to over \$100 million (GPT-4 era). These patterns have direct implications for systems engineering: the compute scaling law determines infrastructure investment timelines, algorithmic efficiency justifies continuous architecture research, and the cost-compute gap shapes build-versus-buy decisions for ML teams. +Three quantitative patterns emerge from this historical data. Training compute for frontier AI models grows approximately 4–5 $\times$ per year, with a doubling time of roughly 5–6 months—about 4 $\times$ faster than Moore's Law for transistor density. Separately, the compute required to achieve a fixed benchmark halves approximately every 8 months for language models due to algorithmic improvements alone. And while compute grows at 4–5 $\times$ per year, training *costs* grow only at roughly 2.4 $\times$ per year, reflecting efficiency gains in hardware utilization and reduced precision arithmetic—though frontier model training costs have still risen from thousands of dollars (LeNet era) to over \$100 million (GPT-4 era). These patterns have direct implications for systems engineering: the compute scaling law determines infrastructure investment timelines, algorithmic efficiency justifies continuous architecture research, and the cost-compute gap shapes build-versus-buy decisions for ML teams. Parallel advances across three dimensions drove these evolutionary trends: data availability, algorithmic innovations, and computing infrastructure. Follow the arrows in @fig-virtuous-cycle to see this reinforcing cycle in motion: more powerful computing infrastructure enabled processing larger datasets, larger datasets drove algorithmic innovations, and better algorithms demanded more sophisticated computing systems. This reinforcing cycle continues to drive progress today. @@ -863,7 +863,7 @@ Algorithmic innovations made it possible to use this data effectively. New metho These algorithmic advances created demand for more powerful computing infrastructure, which evolved in response. On the hardware side, GPUs provided the parallel processing capabilities needed for efficient neural network computation, and specialized AI accelerators like TPUs[^fn-dlprimer-tpu] [@jouppi2023tpu] pushed performance further. High-bandwidth memory systems and fast interconnects addressed data movement challenges. Equally important were software advances: frameworks and libraries[^fn-dl-frameworks] that simplified building and training networks, distributed computing systems that enabled training at scale, and tools for optimizing model deployment. -[^fn-dlprimer-tpu]: **Tensor Processing Unit (TPU)**: Google's custom silicon designed specifically for tensor operations, the mathematical building blocks of neural networks. First deployed internally in 2015, TPUs can perform matrix multiplications up to 30$\times$ faster than 2015-era GPUs while using less power. The name reflects their optimization for tensor operations—multi-dimensional arrays that represent data flowing through neural networks. Google has since made TPUs available through cloud services, democratizing access to this specialized AI hardware. +[^fn-dlprimer-tpu]: **Tensor Processing Unit (TPU)**: Google's custom silicon designed specifically for tensor operations, the mathematical building blocks of neural networks. First deployed internally in 2015, TPUs can perform matrix multiplications up to 30 $\times$ faster than 2015-era GPUs while using less power. The name reflects their optimization for tensor operations—multi-dimensional arrays that represent data flowing through neural networks. Google has since made TPUs available through cloud services, democratizing access to this specialized AI hardware. [^fn-dl-frameworks]: **Deep Learning Frameworks**: Software libraries like TensorFlow and PyTorch that provide high-level abstractions for building and training neural networks. Framework design and selection are detailed in @sec-ml-frameworks. @@ -964,9 +964,9 @@ mnist_input_str = f"{mnist_input_neurons_value}" ::: {.callout-example title="Running Example: MNIST Digit Recognition"} -**The Task**: Given a 28$\times$28 pixel grayscale image of a handwritten digit, classify it as one of the ten digits (0–9). +**The Task**: Given a $28 \times 28$ pixel grayscale image of a handwritten digit, classify it as one of the ten digits (0–9). -**Input Representation**: Each image contains 784 pixels (28$\times$28), with values ranging from 0 (white) to 255 (black). We normalize these to the range [0,1] by dividing by 255. When fed to a neural network, these 784 values form our input vector $\mathbf{x} \in \mathbb{R}^{784}$. +**Input Representation**: Each image contains 784 pixels ($28 \times 28$), with values ranging from 0 (white) to 255 (black). We normalize these to the range [0,1] by dividing by 255. When fed to a neural network, these 784 values form our input vector $\mathbf{x} \in \mathbb{R}^{784}$. **Output Representation**: The network produces 10 values, one for each possible digit. These values represent the network's confidence that the input image contains each digit. The digit with the highest confidence becomes the prediction. @@ -1638,15 +1638,15 @@ mnist_small_mem_kb_str = f"{mnist_small_mem_kb:.0f}" The XOR example established the canonical three-layer architecture, but real-world networks require systematic consideration of design constraints and computational scale[^fn-computational-scale]. Recognizing handwritten digits using the MNIST\index{MNIST Dataset!digit recognition}\index{Dataset!MNIST benchmark} [@lecun1998gradient] dataset illustrates how problem structure determines network dimensions while hidden layer configuration remains an important design decision. -[^fn-computational-scale]: **Computational Scale Considerations**: Network size decisions involve balancing accuracy against computational costs. A 784→`{python} mnist_large_l1`→`{python} mnist_large_l2`→10 MNIST network has ~`{python} mnist_large_params_m_str` M parameters requiring ~`{python} mnist_large_mem_mb_str` MB memory, while a 784→`{python} mnist_small_l1`→`{python} mnist_small_l2`→10 network needs only ~`{python} mnist_small_params_k_str` K parameters and ~`{python} mnist_small_mem_kb_str` KB memory. The larger network might achieve 99.5% vs 98.5% accuracy, but requires 20$\times$ more memory and computation—often an unacceptable trade-off for mobile deployment where every megabyte and millisecond matters. +[^fn-computational-scale]: **Computational Scale Considerations**: Network size decisions involve balancing accuracy against computational costs. A 784→`{python} mnist_large_l1`→`{python} mnist_large_l2`→10 MNIST network has ~`{python} mnist_large_params_m_str` M parameters requiring ~`{python} mnist_large_mem_mb_str` MB memory, while a 784→`{python} mnist_small_l1`→`{python} mnist_small_l2`→10 network needs only ~`{python} mnist_small_params_k_str` K parameters and ~`{python} mnist_small_mem_kb_str` KB memory. The larger network might achieve 99.5% vs 98.5% accuracy, but requires 20 $\times$ more memory and computation—often an unacceptable trade-off for mobile deployment where every megabyte and millisecond matters. -[^fn-mnist-dataset]: **MNIST Dataset**: Created by Yann LeCun and colleagues in 1998, MNIST (Modified National Institute of Standards and Technology) contains 70,000 images of handwritten digits—60,000 for training and 10,000 for testing. Each image is 28$\times$28 pixels in grayscale, totaling 784 features per digit. MNIST became the "hello world" of computer vision, with error rates dropping from 12% with traditional methods in 1998 to 0.23% with modern deep learning. Despite being "solved," MNIST remains invaluable for teaching because it is large enough to be realistic yet small enough to train quickly on any computer. +[^fn-mnist-dataset]: **MNIST Dataset**: Created by Yann LeCun and colleagues in 1998, MNIST (Modified National Institute of Standards and Technology) contains 70,000 images of handwritten digits—60,000 for training and 10,000 for testing. Each image is 28 $\times$ 28 pixels in grayscale, totaling 784 features per digit. MNIST became the "hello world" of computer vision, with error rates dropping from 12% with traditional methods in 1998 to 0.23% with modern deep learning. Despite being "solved," MNIST remains invaluable for teaching because it is large enough to be realistic yet small enough to train quickly on any computer. #### Feedforward Network Architecture {#sec-neural-computation-feedforward-network-architecture-d2cf} Applying the three-layer architecture to MNIST reveals how data characteristics and task requirements constrain network design\index{Feedforward Network!architecture}\index{Neural Network!feedforward}. Compare the two panels in @fig-mnist-topology-1 to see this architecture from both perspectives: panel (a) presents a $28\times 28$ pixel grayscale image of a handwritten digit connected to the hidden and output layers, while panel (b) reveals how the 2D image flattens into a 784-dimensional vector. -The input layer's width is directly determined by our data format. For a 28$\times$28 pixel image, each pixel becomes an input feature, requiring `{python} mnist_input_str` input neurons (28$\times$28 = `{python} mnist_input_str`). We can think of this either as a 2D grid of pixels or as a flattened vector of 784 values, where each value represents the intensity of one pixel. +The input layer's width is directly determined by our data format. For a 28 $\times$ 28 pixel image, each pixel becomes an input feature, requiring `{python} mnist_input_str` input neurons (28 $\times$ 28 = `{python} mnist_input_str`). We can think of this either as a 2D grid of pixels or as a flattened vector of 784 values, where each value represents the intensity of one pixel. The output layer's structure is determined by our task requirements. For digit classification, we use 10 output neurons, one for each possible digit (0-9). When presented with an image, the network produces a value for each output neuron, where higher values indicate greater confidence that the image represents that particular digit. @@ -1691,7 +1691,7 @@ minimum width=46,minimum height=56](\picname){}; picname=C } \def\data{ - % Ovde ide 28×28 = 784 vrednosti piksela (ovde samo primer sa 8×8) + % Ovde ide 28 $\times$ 28 = 784 vrednosti piksela (ovde samo primer sa 8 $\times$ 8) 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -2194,7 +2194,7 @@ training_ratio_str = fmt(training_ratio_calc, precision=1, commas=False) #### Step 3: Training-Only Memory - **Gradients** (same size as parameters): `{python} grad_kib_str` KB -- **Optimizer state** (Adam stores momentum + velocity, 2$\times$ parameters): `{python} opt_kib_str` KB +- **Optimizer state** (Adam stores momentum + velocity, 2 $\times$ parameters): `{python} opt_kib_str` KB **Summary**: @@ -2206,7 +2206,7 @@ training_ratio_str = fmt(training_ratio_calc, precision=1, commas=False) | **Optimizer state** | `{python} opt_kib_str` KB | — | | **Total** | **~`{python} total_train_mib_str` MB** | **~`{python} total_infer_kib_str` KB** | -**Key insight**: Training requires **`{python} MNISTMemory.training_ratio_str`$\times$ more memory** than inference for the same batch size. For larger models, this ratio increases further because gradient and optimizer storage scale with parameter count, while activations scale with batch size $\times$ layer widths. +**Key insight**: Training requires **`{python} MNISTMemory.training_ratio_str` $\times$ more memory** than inference for the same batch size. For larger models, this ratio increases further because gradient and optimizer storage scale with parameter count, while activations scale with batch size $\times$ layer widths. ::: @@ -2268,7 +2268,7 @@ How does the scale of our Lighthouse Models affect the **Data ($D_{vol}$)** term - **MNIST Archetype**: `{python} mnist_params_count_str` parameters $\times$ 4 bytes (FP32) ≈ **`{python} mnist_mem_str` KB**. This entire model fits inside the L2 cache of a modern processor. - **GPT-2 Archetype**: `{python} gpt2_params_count_str` parameters $\times$ 4 bytes (FP32) ≈ **`{python} gpt2_mem_str` GB**. This requires dedicated GPU VRAM and high-speed memory bandwidth. -**The Systems Conclusion**: Moving from ~`{python} mnist_params_k_str`K to `{python} gpt2_params_b_str` B parameters is a **`{python} mem_jump_str`$\times$ jump**. It is not just "more parameters"; it is a phase change in engineering. MNIST is a logic problem; GPT-2 is a **Data Movement** problem. +**The Systems Conclusion**: Moving from ~`{python} mnist_params_k_str`K to `{python} gpt2_params_b_str` B parameters is a **`{python} mem_jump_str` $\times$ jump**. It is not just "more parameters"; it is a phase change in engineering. MNIST is a logic problem; GPT-2 is a **Data Movement** problem. ::: The memory calculations above are precise but slow. Experienced engineers develop shortcuts—*quick estimation for ML engineers*—that enable rapid feasibility assessments. @@ -2318,8 +2318,8 @@ Detailed calculations are essential for design documents, but experienced engine - **Parameters → Bytes**: Multiply by 4 (FP32) or 2 (FP16/BF16) or 1 (INT8) - **FC layer parameters**: Input $\times$ Output (plus Output biases, usually negligible) -- **Training memory**: ~3–4$\times$ inference memory (gradients + optimizer state) -- **Adam optimizer overhead**: 2$\times$ parameter memory (momentum + velocity) +- **Training memory**: ~3–4 $\times$ inference memory (gradients + optimizer state) +- **Adam optimizer overhead**: 2 $\times$ parameter memory (momentum + velocity) - **Max batch size**: (GPU VRAM − Model Size) ÷ (Activations per sample) **Compute Estimation** @@ -2373,6 +2373,7 @@ This equation encapsulates the entire forward pass: the network $f$ takes an inp $$ \text{loss} = \mathcal{L}(\hat{y}, y) $$ {#eq-loss-general} \index{Statistical Decision Theory!loss function origin} + [^fn-loss-function]: **Loss Function**\index{Loss Function!etymology}: "Loss" from Old English *los* (destruction)---it quantifies how far predictions deviate from truth. The formalization traces to Abraham Wald's *statistical decision theory* (1939), where a "loss function" measured the cost of incorrect decisions. Common choices include cross-entropy (classification) and mean squared error (regression), each creating a different optimization landscape that shapes how training proceeds. See @sec-model-training for how loss landscapes interact with optimizer behavior. This error measurement drives the adjustment of network parameters through backpropagation, which we examine in detail below. @@ -2490,7 +2491,7 @@ The forward pass is only half the story. When an image of a handwritten digit enters our network, it undergoes a series of transformations through the layers. Each transformation combines the weighted inputs with learned patterns to progressively extract relevant features. For the 784-128-64-10 digit classifier, a $28\times 28$ pixel image is processed through multiple layers to ultimately produce probabilities for each possible digit (0-9). -The process begins with the input layer, where each pixel's grayscale value becomes an input feature. For MNIST, this means `{python} mnist_input_str` input values (28$\times$28 = `{python} mnist_input_str`), each normalized between 0 and 1. These values then propagate forward through the hidden layers, where each neuron combines its inputs according to its learned weights and applies a nonlinear activation function. +The process begins with the input layer, where each pixel's grayscale value becomes an input feature. For MNIST, this means `{python} mnist_input_str` input values (28 $\times$ 28 = `{python} mnist_input_str`), each normalized between 0 and 1. These values then propagate forward through the hidden layers, where each neuron combines its inputs according to its learned weights and applies a nonlinear activation function. Each forward pass through our MNIST network (784-128-64-10) requires substantial matrix operations. The first layer alone performs nearly 100,000 multiply-accumulate operations per sample. When processing multiple samples in a batch, these operations multiply accordingly, requiring careful management of memory bandwidth and computational resources. Specialized hardware like GPUs executes these operations efficiently through parallel processing. @@ -2662,15 +2663,15 @@ double_ratio_exact_str = fmt(double_ratio_value, precision=2, commas=False) **Solution**: -| **Layer** | **Operation** | **Dimensions** | **Ops** | -|:------------|:---------------|------------------------------------------:|---------------------------------------------------------------:| -| **Layer 1** | MatMul | (32$\times$784) $\times$ (784$\times$128) | 2 $\times$ 32 $\times$ 784 $\times$ 128 = `{python} l1_mm_str` | -| **Layer 1** | Bias + ReLU | 32 $\times$ 128 | 2 $\times$ 4,096 = `{python} l1_bias_str` | -| **Layer 2** | MatMul | (32$\times$128) $\times$ (128$\times$64) | 2 $\times$ 32 $\times$ 128 $\times$ 64 = `{python} l2_mm_str` | -| **Layer 2** | Bias + ReLU | 32 $\times$ 64 | 2 $\times$ 2,048 = `{python} l2_bias_str` | -| **Layer 3** | MatMul | (32$\times$64) $\times$ (64$\times$10) | 2 $\times$ 32 $\times$ 64 $\times$ 10 = `{python} l3_mm_str` | -| **Layer 3** | Bias + Softmax | 32 $\times$ 10 | ~`{python} l3_bias_str` (simplified) | -| **Total** | | | **~`{python} MNISTMemory.total_mops_str` MOps** | +| **Layer** | **Operation** | **Dimensions** | **Ops** | +|:------------|:---------------|----------------------------------------------:|---------------------------------------------------------------:| +| **Layer 1** | MatMul | (32 $\times$ 784) $\times$ (784 $\times$ 128) | 2 $\times$ 32 $\times$ 784 $\times$ 128 = `{python} l1_mm_str` | +| **Layer 1** | Bias + ReLU | 32 $\times$ 128 | 2 $\times$ 4,096 = `{python} l1_bias_str` | +| **Layer 2** | MatMul | (32 $\times$ 128) $\times$ (128 $\times$ 64) | 2 $\times$ 32 $\times$ 128 $\times$ 64 = `{python} l2_mm_str` | +| **Layer 2** | Bias + ReLU | 32 $\times$ 64 | 2 $\times$ 2,048 = `{python} l2_bias_str` | +| **Layer 3** | MatMul | (32 $\times$ 64) $\times$ (64 $\times$ 10) | 2 $\times$ 32 $\times$ 64 $\times$ 10 = `{python} l3_mm_str` | +| **Layer 3** | Bias + Softmax | 32 $\times$ 10 | ~`{python} l3_bias_str` (simplified) | +| **Total** | | | **~`{python} MNISTMemory.total_mops_str` MOps** | **Per-image cost**: `{python} MNISTMemory.total_mops_str` MOps ÷ 32 = **~`{python} MNISTMemory.per_image_kops_str` KOps per image** @@ -2678,7 +2679,7 @@ double_ratio_exact_str = fmt(double_ratio_value, precision=2, commas=False) 1. **Layer 1 dominates**: The first layer accounts for `{python} MNISTMemory.layer1_pct_str`% of all operations because it processes the largest input (784 dimensions). This is why dimensionality reduction in early layers is so impactful. 2. **Compute vs. Memory**: At `{python} MNISTMemory.per_image_kops_str` KOps per image and ~`{python} MNISTMemory.inference_kb_display` KB memory, this network has an arithmetic intensity of ~`{python} MNISTMemory.arith_intensity_str` FLOPs/byte—firmly in the **memory-bound** regime for most hardware (see the **Roofline Model** in @sec-machine-foundations-roofline-model-2529 for how arithmetic intensity determines whether a workload is memory-bound or compute-bound). A modern GPU achieving 10 TFLOPS would process each image in ~22 nanoseconds of pure compute, but memory latency typically dominates actual inference time. -3. **Scaling intuition**: Doubling the hidden layer widths (784→256→128→10) increases $O$ by ~`{python} double_ratio_str`$\times$ to ~`{python} double_total_mops_str` MOps. This comes from recomputing each layer: L1 and L3 double, L2 quadruples, so the total grows by about `{python} double_ratio_exact_str`$\times$ rather than 4$\times$. +3. **Scaling intuition**: Doubling the hidden layer widths (784→256→128→10) increases $O$ by ~`{python} double_ratio_str` $\times$ to ~`{python} double_total_mops_str` MOps. This comes from recomputing each layer: L1 and L3 double, L2 quadruples, so the total grows by about `{python} double_ratio_exact_str` $\times$ rather than 4 $\times$. ::: @@ -2688,9 +2689,9 @@ Forward propagation implementation involves practical considerations that affect Memory management plays a central role during forward propagation. Each layer's activations must be stored for the backward pass during training. For our MNIST example (784-128-64-10) with a batch size of 32, the activation storage requirements are: -* First hidden layer: 32$\times$128 = `{python} MNISTMemory.batch_h1_str` values -* Second hidden layer: 32$\times$64 = `{python} MNISTMemory.batch_h2_str` values -* Output layer: 32$\times$10 = `{python} MNISTMemory.batch_out_str` values +* First hidden layer: 32 $\times$ 128 = `{python} MNISTMemory.batch_h1_str` values +* Second hidden layer: 32 $\times$ 64 = `{python} MNISTMemory.batch_h2_str` values +* Output layer: 32 $\times$ 10 = `{python} MNISTMemory.batch_out_str` values This produces a total of `{python} MNISTMemory.batch_act_total_str` values that must be maintained in memory for each batch during training, consistent with the worked example in @sec-neural-computation-model-size-computational-complexity-1f0f. The memory requirements scale linearly with batch size and become substantial for larger networks. @@ -2698,7 +2699,7 @@ Batch processing introduces important trade-offs. Larger batches enable more eff The organization of computations also affects performance. Matrix operations can be optimized through careful memory layout and specialized libraries. The choice of activation functions affects both the network's learning capabilities and computational efficiency, as some functions (like ReLU) require less computation than others (like tanh or sigmoid). -The computational characteristics of neural networks favor parallel processing architectures\index{GPU!parallel computation}\index{Parallel Processing!neural networks}. While traditional CPUs can execute these operations, GPUs designed for parallel computation achieve substantial speedups, often 10–100$\times$ faster for matrix operations. Specialized AI accelerators achieve even better efficiency through reduced precision arithmetic, specialized memory architectures, and dataflow optimizations tailored for neural network computation patterns. +The computational characteristics of neural networks favor parallel processing architectures\index{GPU!parallel computation}\index{Parallel Processing!neural networks}. While traditional CPUs can execute these operations, GPUs designed for parallel computation achieve substantial speedups, often 10–100 $\times$ faster for matrix operations. Specialized AI accelerators achieve even better efficiency through reduced precision arithmetic, specialized memory architectures, and dataflow optimizations tailored for neural network computation patterns. Energy consumption also varies significantly across hardware platforms\index{Energy Consumption!hardware platforms}\index{Throughput!accelerator parallelism}. CPUs offer flexibility but consume more energy per operation. GPUs provide high throughput at higher power consumption. Specialized edge accelerators optimize for energy efficiency, achieving the same computations with orders of magnitude less power, which is important for mobile and embedded deployments. This energy disparity stems from the memory hierarchy constraints where data movement dominates computation costs. @@ -2891,14 +2892,14 @@ We established earlier that training requires storing activations for backpropag Consider a larger variant of our MNIST network (784→512→256→10) with a batch size of 32. Each layer's activations must be maintained: -* Input layer: 32$\times$784 values (~`{python} bp_input_kb_str` KB using 32-bit numbers) -* Hidden layer 1: 32$\times$512 values (~`{python} bp_h1_kb_str` KB) -* Hidden layer 2: 32$\times$256 values (~`{python} bp_h2_kb_str` KB) -* Output layer: 32$\times$10 values (~`{python} bp_out_kb_str` KB) +* Input layer: 32 $\times$ 784 values (~`{python} bp_input_kb_str` KB using 32-bit numbers) +* Hidden layer 1: 32 $\times$ 512 values (~`{python} bp_h1_kb_str` KB) +* Hidden layer 2: 32 $\times$ 256 values (~`{python} bp_h2_kb_str` KB) +* Output layer: 32 $\times$ 10 values (~`{python} bp_out_kb_str` KB) Beyond activations, we must store gradients for each parameter. For this larger network with approximately `{python} bp_total_params_str` parameters, gradient storage requires several megabytes. Advanced optimizers like Adam\index{Adam Optimizer!momentum and velocity}\index{Optimizer!Adam}[^fn-adam-optimizer] roughly double this by maintaining momentum and velocity terms for every parameter. -[^fn-adam-optimizer]: **Adam Optimizer**: An acronym for "Adaptive Moment Estimation," introduced by Kingma and Ba [@kingma2014adam]. The name reflects how Adam adapts learning rates using running averages (moments) of gradients. "Moment" comes from statistics, where the first moment is the mean and second moment relates to variance. Adam combines AdaGrad's adaptive rates with RMSprop's exponential averaging. It requires 2$\times$ memory (storing momentum and velocity per parameter) but became the default optimizer due to its robustness across diverse problems with minimal tuning. +[^fn-adam-optimizer]: **Adam Optimizer**: An acronym for "Adaptive Moment Estimation," introduced by Kingma and Ba [@kingma2014adam]. The name reflects how Adam adapts learning rates using running averages (moments) of gradients. "Moment" comes from statistics, where the first moment is the mean and second moment relates to variance. Adam combines AdaGrad's adaptive rates with RMSprop's exponential averaging. It requires 2 $\times$ memory (storing momentum and velocity per parameter) but became the default optimizer due to its robustness across diverse problems with minimal tuning. Memory bandwidth compounds these capacity requirements. Each training step requires loading all parameters, storing gradients, and accessing activations—creating substantial memory traffic that scales with both model size and batch size. For modest networks like our MNIST example, this traffic remains manageable, but as models grow, memory bandwidth becomes the primary bottleneck, requiring specialized high-bandwidth memory systems. @@ -3118,7 +3119,7 @@ These computational differences manifest directly in hardware requirements and d [^fn-edge-accelerators]: **Edge AI Accelerators**: Specialized processors like Google's Edge TPU optimize for inference efficiency. As a representative snapshot, such accelerators can reach on the order of a few TOPS/W on INT8-style kernels, which can translate to order-of-magnitude energy-efficiency improvements over CPU execution for well-matched workloads. This efficiency enables deployment on battery-powered devices like smartphones and IoT sensors. -[^fn-inference-precision]: **Inference Numerical Precision**: Inference systems often use reduced precision arithmetic—16-bit or 8-bit numbers instead of 32-bit—to increase throughput while maintaining accuracy. This precision reduction exploits the fact that trained models are more robust to numerical approximation than the training process itself. Using 8-bit integers can provide 4$\times$ throughput improvement compared to 32-bit floating-point operations. +[^fn-inference-precision]: **Inference Numerical Precision**: Inference systems often use reduced precision arithmetic—16-bit or 8-bit numbers instead of 32-bit—to increase throughput while maintaining accuracy. This precision reduction exploits the fact that trained models are more robust to numerical approximation than the training process itself. Using 8-bit integers can provide 4 $\times$ throughput improvement compared to 32-bit floating-point operations. ::: {#fig-training-vs-inference fig-env="figure" fig-pos="htb" fig-cap="**Inference vs. Training Flow**: During inference, neural networks utilize learned weights for forward pass computation only, simplifying the data flow and reducing computational cost compared to training, which requires both forward and backward passes for weight updates. This streamlined process enables efficient deployment of trained models for real-time predictions." fig-alt="Two parallel diagrams comparing inference and training. Both show stacked rectangles representing batches feeding into network layers and output nodes. Inference section shows smaller varied batch sizes with dashed outlines. Training section shows larger fixed batches with solid outlines. Network architecture identical in both with fully connected layers."} ```{.tikz} @@ -3319,7 +3320,7 @@ These architectural differences translate directly into distinct resource profil | **Computational Flow** | Structured for gradient computation preparation | Optimized for direct output generation | | **Resource Profile** | Higher memory requirements for training operations | Minimized memory footprint for efficient execution | -: **Training vs. Inference Forward Pass**: Although both phases execute identical mathematical operations layer-by-layer, they differ fundamentally in memory management. Training must preserve all intermediate activations for gradient computation during the backward pass; inference can discard each layer's outputs immediately after computing the next layer, enabling aggressive memory optimization. This distinction explains why training requires 2–4$\times$ more memory than inference for the same model. {#tbl-forward-pass-comparison} +: **Training vs. Inference Forward Pass**: Although both phases execute identical mathematical operations layer-by-layer, they differ fundamentally in memory management. Training must preserve all intermediate activations for gradient computation during the backward pass; inference can discard each layer's outputs immediately after computing the next layer, enabling aggressive memory optimization. This distinction explains why training requires 2–4 $\times$ more memory than inference for the same model. {#tbl-forward-pass-comparison} #### Memory and Computational Resources {#sec-neural-computation-memory-computational-resources-d02b} @@ -3335,7 +3336,7 @@ Memory requirements\index{Memory Footprint!parameter storage}\index{Memory Footp * Layer 3: 64 $\times$ 10 = `{python} MNISTMemory.w3_str` weights + `{python} MNISTMemory.b3_str` biases * Total: `{python} MNISTMemory.total_params_str` parameters (≈ `{python} MNISTMemory.param_mem_str` KB at 32-bit floating point precision[^fn-floating-point]) -[^fn-floating-point]: **32-bit Floating Point Precision**: Also called "single precision" or FP32, this IEEE 754 standard uses 32 bits to represent real numbers: 1 bit for sign, 8 bits for exponent, and 23 bits for mantissa. While neural network training typically requires FP32 precision to maintain gradient stability, inference often works with FP16 (half precision) or even INT8 (8-bit integers), reducing memory usage by 2$\times$ to 4$\times$. Modern AI chips like Google's TPU v4 support "bfloat16" (brain floating point), a custom 16-bit format that maintains FP32's range while halving memory requirements. @sec-machine-foundations-numerical-representations-c889 provides a detailed comparison of numerical formats (FP32, FP16, BF16, FP8, INT8) and their trade-offs between precision, dynamic range, and throughput. +[^fn-floating-point]: **32-bit Floating Point Precision**: Also called "single precision" or FP32, this IEEE 754 standard uses 32 bits to represent real numbers: 1 bit for sign, 8 bits for exponent, and 23 bits for mantissa. While neural network training typically requires FP32 precision to maintain gradient stability, inference often works with FP16 (half precision) or even INT8 (8-bit integers), reducing memory usage by 2 $\times$ to 4 $\times$. Modern AI chips like Google's TPU v4 support "bfloat16" (brain floating point), a custom 16-bit format that maintains FP32's range while halving memory requirements. @sec-machine-foundations-numerical-representations-c889 provides a detailed comparison of numerical formats (FP32, FP16, BF16, FP8, INT8) and their trade-offs between precision, dynamic range, and throughput. 2. Dynamic Memory (Activations per image): * Layer 1 output: 128 values @@ -3388,7 +3389,7 @@ Before examining how these concepts integrate in a real-world deployment, verify - [ ] Can you trace how architectural decisions (layer sizes, activation functions) impact both training dynamics and inference performance? - [ ] Do you understand how parameter counts translate to memory requirements across training and inference phases? -- [ ] Can you explain why the same network requires 2–3$\times$ more memory during training than inference? +- [ ] Can you explain why the same network requires 2–3 $\times$ more memory during training than inference? **Training to Deployment:** @@ -3413,7 +3414,7 @@ Before examining how these concepts integrate in a real-world deployment, verify - [ ] Can you trace a single input (e.g., MNIST digit image) through the complete system: raw input → preprocessing → forward propagation through layers → output probabilities → post-processing → final prediction? - [ ] Do you understand the distinction between what happens once (loading trained weights) versus per-input (forward propagation)? -**Self-Test**: For an MNIST digit classifier (`{python} mnist_arch_str`) deployed in production: (1) Using the memory analysis from this chapter, explain why training requires ~`{python} MNISTMemory.training_ratio_str`$\times$ more memory than inference, and identify which components (gradients, optimizer state, activations) contribute to this difference. (2) Trace a single digit image from camera capture through preprocessing, inference, and post-processing to final prediction. (3) Identify where bottlenecks might occur in a real-time system processing 100 images/second. (4) Describe how you would monitor for model degradation in production. +**Self-Test**: For an MNIST digit classifier (`{python} mnist_arch_str`) deployed in production: (1) Using the memory analysis from this chapter, explain why training requires ~`{python} MNISTMemory.training_ratio_str` $\times$ more memory than inference, and identify which components (gradients, optimizer state, activations) contribute to this difference. (2) Trace a single digit image from camera capture through preprocessing, inference, and post-processing to final prediction. (3) Identify where bottlenecks might occur in a real-time system processing 100 images/second. (4) Describe how you would monitor for model degradation in production. *The following case study demonstrates how these concepts integrate in a production system deployed at massive scale. Pay attention to how architectural choices, training strategies, and deployment constraints combine to create a working ML system.* @@ -3557,9 +3558,9 @@ Neural network-based ZIP code recognition transformed USPS mail processing opera | **Training time** | 3 days (Sun-4/260) | N/A | | **Training epochs** | 23 | N/A | -: **USPS LeNet Deployment Results**: LeNet achieved lower error rates than human operators (1.0% vs. 2.5%) while processing digits 10–30$\times$ faster—demonstrating that neural networks could surpass human performance on constrained pattern recognition tasks even with 1989-era hardware. The 9% rejection rate represents the optimal economic balance between automation throughput and misrouting cost. {#tbl-usps-numbers} +: **USPS LeNet Deployment Results**: LeNet achieved lower error rates than human operators (1.0% vs. 2.5%) while processing digits 10–30 $\times$ faster—demonstrating that neural networks could surpass human performance on constrained pattern recognition tasks even with 1989-era hardware. The 9% rejection rate represents the optimal economic balance between automation throughput and misrouting cost. {#tbl-usps-numbers} -**Key insight**: The neural network achieved *better accuracy than humans* (1.0% vs 2.5% error) while processing 10–30$\times$ faster. The 9% rejection rate represented the economically optimal trade-off: digits the network was uncertain about went to human operators rather than risking misrouted mail. +**Key insight**: The neural network achieved *better accuracy than humans* (1.0% vs 2.5% error) while processing 10–30 $\times$ faster. The 9% rejection rate represented the economically optimal trade-off: digits the network was uncertain about went to human operators rather than risking misrouted mail. **Economic impact**: By the late 1990s, LeNet-based systems were reading millions of checks per day at financial institutions, and the USPS system processed over 10% of all handwritten mail in the United States, demonstrating neural networks' viability for mission-critical, high-volume applications. @@ -3583,19 +3584,19 @@ The success of this early large-scale neural network deployment helped establish The same neural network computation that required industrial-scale infrastructure in 1990 runs on pocket-sized devices today. @tbl-then-vs-now quantifies four decades of progress: -| **Aspect** | **1990s USPS System** | **2025 Equivalent** | **Improvement** | -|:----------------------|----------------------------------:|----------------------------------------------:|-----------------------:| -| **Hardware cost** | ~\$50,000 (Sun-4 workstation) | ~\$50 (Raspberry Pi 5) | 1,000$\times$ | -| **Inference latency** | ~100 ms/digit | ~0.1 ms/digit | 1,000$\times$ | -| **Power consumption** | 50–100 W | 5 W | 10–20$\times$ | -| **Training time** | 3 days | ~30 seconds | 8,640$\times$ | -| **Model storage** | ~`{python} lenet_1_mem_kb_str` KB | ~`{python} lenet_1_mem_kb_str` KB (unchanged) | 1$\times$ (same model) | -| **Energy/inference** | ~10 J | ~0.5 mJ | 20,000$\times$ | -| **\$/inference** | ~\$0.001 | ~\$0.000001 | 1,000$\times$ | +| **Aspect** | **1990s USPS System** | **2025 Equivalent** | **Improvement** | +|:----------------------|----------------------------------:|----------------------------------------------:|------------------------:| +| **Hardware cost** | ~\$50,000 (Sun-4 workstation) | ~\$50 (Raspberry Pi 5) | 1,000 $\times$ | +| **Inference latency** | ~100 ms/digit | ~0.1 ms/digit | 1,000 $\times$ | +| **Power consumption** | 50–100 W | 5 W | 10–20 $\times$ | +| **Training time** | 3 days | ~30 seconds | 8,640 $\times$ | +| **Model storage** | ~`{python} lenet_1_mem_kb_str` KB | ~`{python} lenet_1_mem_kb_str` KB (unchanged) | 1 $\times$ (same model) | +| **Energy/inference** | ~10 J | ~0.5 mJ | 20,000 $\times$ | +| **\$/inference** | ~\$0.001 | ~\$0.000001 | 1,000 $\times$ | -: **Hardware Progress for Neural Network Computation**: The same LeNet computation that required a \$50,000 workstation in 1990 runs on a \$50 Raspberry Pi today—1,000$\times$ cheaper, 1,000$\times$ faster, and 20,000$\times$ more energy-efficient. Crucially, the algorithm is unchanged; all improvement came from hardware. This validates the systems principle that algorithm-hardware co-design multiplies gains across both dimensions. {#tbl-then-vs-now} +: **Hardware Progress for Neural Network Computation**: The same LeNet computation that required a \$50,000 workstation in 1990 runs on a \$50 Raspberry Pi today—1,000 $\times$ cheaper, 1,000 $\times$ faster, and 20,000 $\times$ more energy-efficient. Crucially, the algorithm is unchanged; all improvement came from hardware. This validates the systems principle that algorithm-hardware co-design multiplies gains across both dimensions. {#tbl-then-vs-now} -**What changed**: Hardware improved by 1,000–10,000$\times$ across every metric *except* the algorithm. LeNet's architecture remains essentially unchanged. This validates a key systems principle: **algorithm-hardware co-design** means improvements in either dimension multiply together. +**What changed**: Hardware improved by 1,000–10,000 $\times$ across every metric *except* the algorithm. LeNet's architecture remains essentially unchanged. This validates a key systems principle: **algorithm-hardware co-design** means improvements in either dimension multiply together. **What stayed the same**: The core engineering challenges persist. Modern smartphone OCR still requires preprocessing for lighting variation, confidence thresholds for uncertain predictions, and fallback to human review for edge cases. The USPS system's architecture (capture, preprocess, inference, postprocess, action) remains the template for every production ML pipeline. @@ -3638,7 +3639,7 @@ Teams assume automatic feature learning removes the need for domain knowledge. S **Pitfall:** *Using neural networks for problems solvable with simpler methods.* -Teams assume deep learning always performs better. Logistic regression training in 10 ms often outperforms a neural network requiring 2 hours when data contains fewer than 1,000 examples or relationships are approximately linear. If logistic regression achieves 94% accuracy, a neural network achieving 95% rarely justifies the cost: 100–1,000$\times$ longer training, 10–50$\times$ more memory, and ongoing maintenance burden. As shown in @sec-neural-computation-learning-process-0b83, neural networks excel at hierarchical pattern discovery but impose substantial overhead. Reserve them for problems with spatial locality, temporal dependencies, or high-dimensional nonlinear interactions that simpler models cannot capture. +Teams assume deep learning always performs better. Logistic regression training in 10 ms often outperforms a neural network requiring 2 hours when data contains fewer than 1,000 examples or relationships are approximately linear. If logistic regression achieves 94% accuracy, a neural network achieving 95% rarely justifies the cost: 100–1,000 $\times$ longer training, 10–50 $\times$ more memory, and ongoing maintenance burden. As shown in @sec-neural-computation-learning-process-0b83, neural networks excel at hierarchical pattern discovery but impose substantial overhead. Reserve them for problems with spatial locality, temporal dependencies, or high-dimensional nonlinear interactions that simpler models cannot capture. \index{Class Imbalance!training failure} **Pitfall:** *Training neural networks without analyzing data distribution characteristics.* @@ -3652,11 +3653,11 @@ Data scientists develop models with unlimited time budgets, assuming deployment \index{Arithmetic Intensity!GPU utilization} **Pitfall:** *Assuming more compute automatically means faster training.* -Teams purchase expensive GPUs expecting proportional speedups, then discover workloads are memory-bound. Arithmetic intensity determines which resource constrains performance. As shown in @tbl-historical-performance, small networks like MNIST (784 to 128 to 64 to 10) have arithmetic intensity of approximately 0.5 FLOPs/byte, well below the approximately 100 FLOPs/byte threshold where GPUs achieve peak utilization. For memory-bound workloads, a \$200 CPU matches a \$10,000 GPU; for compute-bound GPT-scale models, the GPU provides 100$\times$ speedup. This mismatch explains why teams report GPU utilization rates from 5% to 80% depending on model architecture. +Teams purchase expensive GPUs expecting proportional speedups, then discover workloads are memory-bound. Arithmetic intensity determines which resource constrains performance. As shown in @tbl-historical-performance, small networks like MNIST (784 to 128 to 64 to 10) have arithmetic intensity of approximately 0.5 FLOPs/byte, well below the approximately 100 FLOPs/byte threshold where GPUs achieve peak utilization. For memory-bound workloads, a \$200 CPU matches a \$10,000 GPU; for compute-bound GPT-scale models, the GPU provides 100 $\times$ speedup. This mismatch explains why teams report GPU utilization rates from 5% to 80% depending on model architecture. **Pitfall:** *Extrapolating accuracy improvements without considering diminishing returns.* -Teams observe that scaling from 10K to 100K parameters improves accuracy by 5 percentage points, then assume scaling to 1M parameters yields another 5 points. Neural network accuracy follows logarithmic scaling: each order of magnitude in compute yields diminishing returns. As shown in @tbl-historical-performance, moving from LeNet-5 (60K parameters) to modern architectures required approximately $10^{11}$$\times$ more training FLOPs to reduce ImageNet error from 26% to 3%, roughly 3 percentage points per order of magnitude. Achieving 99% accuracy might cost 10$\times$ more than 98%, and 99.9% might cost 100$\times$ more than 99%. Teams that fail to model this relationship overpromise accuracy and underestimate resources. +Teams observe that scaling from 10K to 100K parameters improves accuracy by 5 percentage points, then assume scaling to 1M parameters yields another 5 points. Neural network accuracy follows logarithmic scaling: each order of magnitude in compute yields diminishing returns. As shown in @tbl-historical-performance, moving from LeNet-5 (60K parameters) to modern architectures required approximately $10^{11}$ $\times$ more training FLOPs to reduce ImageNet error from 26% to 3%, roughly 3 percentage points per order of magnitude. Achieving 99% accuracy might cost 10 $\times$ more than 98%, and 99.9% might cost 100 $\times$ more than 99%. Teams that fail to model this relationship overpromise accuracy and underestimate resources. These fallacies and pitfalls share a common root: applying intuitions from deterministic software engineering to probabilistic learning systems. Recognizing them early saves weeks of misdirected effort and prevents production failures that are expensive to diagnose after deployment. @@ -3666,7 +3667,7 @@ We opened this chapter with a question: why do deep learning systems engineers n Neural networks transform computational approaches by replacing rule-based programming with adaptive systems that learn patterns from data. Building on the biological-to-artificial neuron mappings explored throughout this chapter, these systems process complex information and improve performance through experience. -Neural network architecture demonstrates hierarchical processing, where each layer extracts progressively more abstract patterns from raw data. Training adjusts connection weights through iterative optimization to minimize prediction errors, while inference applies learned knowledge to make predictions on new data. This separation between learning and application phases creates distinct system requirements for computational resources, memory usage, and processing latency that shape system design and deployment strategies. Training requires ~`{python} MNISTMemory.training_ratio_str`$\times$ more memory than inference because gradients, optimizer state, and activations must be stored and updated. The USPS digit recognition case study demonstrated that these mathematical principles combine into production systems where the complete pipeline—preprocessing, neural inference, and post-processing—must operate within real-world latency and reliability constraints. +Neural network architecture demonstrates hierarchical processing, where each layer extracts progressively more abstract patterns from raw data. Training adjusts connection weights through iterative optimization to minimize prediction errors, while inference applies learned knowledge to make predictions on new data. This separation between learning and application phases creates distinct system requirements for computational resources, memory usage, and processing latency that shape system design and deployment strategies. Training requires ~`{python} MNISTMemory.training_ratio_str` $\times$ more memory than inference because gradients, optimizer state, and activations must be stored and updated. The USPS digit recognition case study demonstrated that these mathematical principles combine into production systems where the complete pipeline—preprocessing, neural inference, and post-processing—must operate within real-world latency and reliability constraints. ```{python} #| label: mnist-weights-calc @@ -3697,13 +3698,13 @@ mnist_pixels_str = f"{mnist_pixels_value}" fc1_neurons_str = f"{fc1_neurons_value}" ``` -The running MNIST example made this escalation tangible: the same 28 $\times$ 28 digit that required ~`{python} rb_ops_str` rule-based comparisons demanded `{python} dl_total_macs_str` MACs in even a modest three-layer network—a `{python} dl_ops_ratio_str`$\times$ increase that generalizes across the systems dimensions captured in @tbl-evolution. These fundamentals primarily develop the **Algorithm** axis of the D·A·M taxonomy while revealing how algorithmic choices propagate into **Machine** constraints. +The running MNIST example made this escalation tangible: the same 28 $\times$ 28 digit that required ~`{python} rb_ops_str` rule-based comparisons demanded `{python} dl_total_macs_str` MACs in even a modest three-layer network—a `{python} dl_ops_ratio_str` $\times$ increase that generalizes across the systems dimensions captured in @tbl-evolution. These fundamentals primarily develop the **Algorithm** axis of the D·A·M taxonomy while revealing how algorithmic choices propagate into **Machine** constraints. The mathematical and systems implications emerge through fully connected architectures. The multilayer perceptrons explored here demonstrate universal function approximation: with enough neurons and appropriate weights, such networks can theoretically learn any continuous function. This mathematical generality comes with computational costs. Consider our MNIST example: a 28 $\times$ 28 pixel image contains `{python} mnist_pixels_str` input values, and a fully connected network treats each pixel independently, learning over `{python} fc1_weights_str` weights in the first layer alone (`{python} mnist_pixels_str` inputs $\times$ `{python} fc1_neurons_str` neurons). Neighboring pixels are highly correlated while distant pixels rarely interact. Fully connected architectures expend computational resources learning irrelevant long-range relationships. ::: {.callout-takeaways title="The Math Behind the Model"} -* **Each paradigm shift buys representation power at exponential systems cost**: Classifying the same 28 $\times$ 28 digit escalates from ~`{python} rb_ops_str` comparisons (rule-based) through ~`{python} hog_ops_approx_str` structured operations (classical ML) to `{python} dl_total_macs_str` matrix MACs (deep learning)—a `{python} dl_ops_ratio_str`$\times$ increase that reshapes hardware requirements at every level. +* **Each paradigm shift buys representation power at exponential systems cost**: Classifying the same 28 $\times$ 28 digit escalates from ~`{python} rb_ops_str` comparisons (rule-based) through ~`{python} hog_ops_approx_str` structured operations (classical ML) to `{python} dl_total_macs_str` matrix MACs (deep learning)—a `{python} dl_ops_ratio_str` $\times$ increase that reshapes hardware requirements at every level. * **Neural networks learn patterns, not rules**: These networks replace hand-coded features with hierarchical representations discovered from data. The system adapts to the problem rather than requiring manual engineering. * **Training and inference have opposite priorities**: Training optimizes throughput (large batches, hours of compute); inference optimizes latency (single samples, milliseconds). Effective systems account for both phases in their design. * **Activation function choice is both a mathematical and a hardware decision**: ReLU dominates because $\max(0,x)$ is orders of magnitude cheaper than $\exp(x)$, and its constant gradient for positive inputs prevents the vanishing gradient problem that plagues sigmoid and tanh in deep networks. diff --git a/book/quarto/contents/vol1/optimizations/model_compression.qmd b/book/quarto/contents/vol1/optimizations/model_compression.qmd index 09a72eefe6..eb45fce1ff 100644 --- a/book/quarto/contents/vol1/optimizations/model_compression.qmd +++ b/book/quarto/contents/vol1/optimizations/model_compression.qmd @@ -21,7 +21,7 @@ engine: jupyter ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{30}{25}{90}{25}{45}{0}{0}{15} +\mlsysstack{30}{25}{90}{25}{45}{0}{0}{10} \end{marginfigure} _Why do the models that win benchmarks rarely become the models that run in production?_ @@ -212,7 +212,7 @@ Model optimization is not a single technique but a *framework* with three comple The top layer, *efficient model representation*, focuses on eliminating redundancy in the model structure. Techniques like pruning, knowledge distillation, and Neural Architecture Search[^fn-nas] (NAS) reduce the number of parameters or operations required, addressing memory footprint and computational complexity at the algorithmic level. -[^fn-nas]: **Neural Architecture Search (NAS)**\index{NAS!etymology}: Pioneered by Barret Zoph and Quoc V. Le at Google Brain [@zoph2017neural], who used reinforcement learning to *learn* the architecture itself. The initial cost was staggering: 800 GPUs for 28 days. This expense catalyzed efficient NAS: weight-sharing approaches (ENAS, 2018) reduced search cost by 1000$\times$, while hardware-aware NAS [@tan2019mnasnet] incorporated latency targets directly into the search objective. The resulting architectures---EfficientNet, MobileNetV3, NASNet---consistently outperform hand-designed models, demonstrating that architectural inductive biases (see @sec-network-architectures) can themselves be learned. +[^fn-nas]: **Neural Architecture Search (NAS)**\index{NAS!etymology}: Pioneered by Barret Zoph and Quoc V. Le at Google Brain [@zoph2017neural], who used reinforcement learning to *learn* the architecture itself. The initial cost was staggering: 800 GPUs for 28 days. This expense catalyzed efficient NAS: weight-sharing approaches (ENAS, 2018) reduced search cost by 1000 $\times$, while hardware-aware NAS [@tan2019mnasnet] incorporated latency targets directly into the search objective. The resulting architectures---EfficientNet, MobileNetV3, NASNet---consistently outperform hand-designed models, demonstrating that architectural inductive biases (see @sec-network-architectures) can themselves be learned. The middle layer, *efficient numerics representation*, optimizes how numerical values are stored and processed. Quantization and mixed-precision\index{Mixed-Precision Training} training reduce the bit-width of weights and activations (e.g., from 32-bit floating point to 8-bit integers), enabling faster execution and lower memory usage on specialized hardware. @@ -234,11 +234,11 @@ According to the **Iron Law** ($T = \frac{D_{vol}}{BW} + \frac{O}{R_{peak} \cdot | **Operation** | **Bit-Width** | **Relative Energy** | |:----------------|--------------:|--------------------:| -| **Integer Add** | 8-bit | 1$\times$ | -| **Float Add** | 32-bit | 30$\times$ | -| **DRAM Read** | 64-bit | **40,000$\times$** | +| **Integer Add** | 8-bit | 1 $\times$ | +| **Float Add** | 32-bit | 30 $\times$ | +| **DRAM Read** | 64-bit | **40,000 $\times$** | -**For Inference**: Moving from FP32 to INT8 doesn't just save 4$\times$ memory; it can reduce the **energy per inference** by up to **`{python} int8_energy_reduction_str`$\times$** on hardware with dedicated INT8 units, depending on the compute-to-memory ratio of the workload. +**For Inference**: Moving from FP32 to INT8 doesn't just save 4 $\times$ memory; it can reduce the **energy per inference** by up to **`{python} int8_energy_reduction_str` $\times$** on hardware with dedicated INT8 units, depending on the compute-to-memory ratio of the workload. This is the difference between a battery lasting 1 hour or 20 hours. @@ -405,7 +405,7 @@ We call this phenomenon *the quantization speedup*. 2. **New Size**: `{python} params_b_str` $\times 10^9$ $\times$ `{python} bytes_int4_str` = `{python} int4_size_str` GB. 3. **New Speed**: Loading `{python} int4_size_str` GB takes **`{python} int4_latency_str` ms**. Speed jumps to **`{python} int4_toks_str` tokens/sec**. -**The Conclusion**: Quantization is not just about "fitting" the model; it is a **`{python} speedup_str`$\times$ Linear Speedup** because LLM generation is bandwidth-bound. +**The Conclusion**: Quantization is not just about "fitting" the model; it is a **`{python} speedup_str` $\times$ Linear Speedup** because LLM generation is bandwidth-bound. ::: The relative importance of each dimension varies by deployment target. Cloud systems may tolerate larger models but demand throughput; mobile devices prioritize memory and energy; embedded systems face hard constraints on all resources simultaneously. Understanding these deployment contexts shapes which optimization dimensions to prioritize. @@ -426,19 +426,19 @@ The optimization framework above identifies three dimensions of compression, but Cloud inference centers on throughput (requests/second/dollar), where quantization enables serving more concurrent requests and operator fusion reduces per-request latency [@choudhary2020comprehensive; @dean2018new]. Mobile and edge deployments must fit device memory while meeting real-time targets. A camera app processing 30 fps has 33 ms per frame, so any optimization reducing inference below this threshold directly improves user experience. -TinyML\index{TinyML!model compression}[^fn-microcontroller-constraints] makes optimization existential, not optional. A microcontroller with 256KB RAM cannot run a 100MB model regardless of accuracy. The model must compress below hardware limits or deployment is impossible [@banbury2020benchmarking]. Even on mobile devices with comparatively generous resources, a single optimization technique can deliver a *4$\times$ performance win* that means the difference between a feature that ships and one that never leaves the prototype stage. +TinyML\index{TinyML!model compression}[^fn-microcontroller-constraints] makes optimization existential, not optional. A microcontroller with 256KB RAM cannot run a 100MB model regardless of accuracy. The model must compress below hardware limits or deployment is impossible [@banbury2020benchmarking]. Even on mobile devices with comparatively generous resources, a single optimization technique can deliver a *4 $\times$ performance win* that means the difference between a feature that ships and one that never leaves the prototype stage. \index{MobileNetV3!NAS-optimized architecture} -::: {.callout-example title="The 4$\times$ MobileNet Win"} +::: {.callout-example title="The 4 $\times$ MobileNet Win"} **The Context**: A mobile app wants to add real-time "Background Blur" to video calls. The feature requires a segmentation model running at 30 FPS. **The Bottleneck**: The unoptimized MobileNetV3 (FP32), a NAS-optimized successor to our MobileNetV2 lighthouse model, runs at 8 FPS on mid-tier Android phones. It is too slow to ship. **The Optimization**: -1. **Quantization**: Converting weights to INT8 reduces size by 4$\times$ and uses the phone's DSP/NPU. -2. **Result**: Speed jumps to 35 FPS. Energy per frame drops by 3$\times$. +1. **Quantization**: Converting weights to INT8 reduces size by 4 $\times$ and uses the phone's DSP/NPU. +2. **Result**: Speed jumps to 35 FPS. Energy per frame drops by 3 $\times$. **The Business Value**: Compression did not just "optimize" the feature; it **enabled** it. Without INT8 quantization, the product simply could not exist for the target market. ::: @@ -531,7 +531,7 @@ dscnn_tiny_str = "ok" | **MobileNetV2 (INT8)** | `{python} mobilenet_int8_str` | `{python} mobilenet_int8_str` | ok | ok | `{python} mobilenet_int8_tiny_str` | | **DS-CNN (KWS)** | `{python} dscnn_str` | `{python} dscnn_str` | ok | ok | `{python} dscnn_tiny_str` | -: **The Deployment Gap**: Model memory requirements compared against typical device capacities. Even MobileNetV2 quantized to INT8 exceeds TinyML constraints by 7$\times$, while the purpose-built DS-CNN keyword spotter fits comfortably. Numbers in parentheses show how many times the model exceeds device memory. {#tbl-model-vs-device} +: **The Deployment Gap**: Model memory requirements compared against typical device capacities. Even MobileNetV2 quantized to INT8 exceeds TinyML constraints by 7 $\times$, while the purpose-built DS-CNN keyword spotter fits comfortably. Numbers in parentheses show how many times the model exceeds device memory. {#tbl-model-vs-device} \index{Environmental Impact!model compression benefits} As @tbl-model-vs-device makes concrete, even aggressively optimized models like MobileNetV2 at INT8 precision exceed TinyML device memory by an order of magnitude. Optimization also contributes to sustainable and accessible AI deployment. Reducing a model's energy footprint is important as AI workloads scale, helping mitigate the environmental impact of large-scale ML training and inference [@patterson2021carbon]. At the same time, optimized models can expand the reach of machine learning, supporting applications in low-resource environments, from rural healthcare to autonomous systems operating in the field. @@ -559,16 +559,16 @@ Optimization is a search for the **Pareto Frontier**. @tbl-optimization-tradeoffs summarizes the key optimization techniques, their systems benefits, and their ML costs. These are empirical relationships—actual results depend on model architecture, task, and careful implementation. -| **Technique** | **Systems Gain** | **ML Cost** | **Typical Impact** | **Region** | -|:-----------------------------|:-----------------------------------------|:---------------------|:---------------------|:----------:| -| **Operator Fusion** | 10–30% latency reduction | None | No accuracy loss | 1 | -| **FP32 → BF16** | 2$\times$ memory, ~2$\times$ throughput | Minimal | <0.1% accuracy drop | 1 | -| **FP16 → INT8** | 2$\times$ memory, 2–4$\times$ throughput | Quantization error | 0.5–1% accuracy drop | 2 | -| **50% Pruning** | ~2$\times$ smaller model | Capacity loss | 0.5–1% accuracy drop | 2 | -| **Knowledge Distillation** | 2–10$\times$ smaller student | Capability ceiling | 1–3% accuracy drop | 2 | -| **4-bit Quantization** | 4$\times$ memory reduction | Significant error | 2–5% accuracy drop | 2–3 | -| **90% Pruning** | ~10$\times$ smaller model | Severe capacity loss | 5–15% accuracy drop | 3 | -| **↑ Batch Size (8$\times$)** | Higher throughput, better GPU util | Generalization gap | Requires LR scaling | — | +| **Technique** | **Systems Gain** | **ML Cost** | **Typical Impact** | **Region** | +|:------------------------------|:-------------------------------------------|:---------------------|:---------------------|:----------:| +| **Operator Fusion** | 10–30% latency reduction | None | No accuracy loss | 1 | +| **FP32 → BF16** | 2 $\times$ memory, ~2 $\times$ throughput | Minimal | <0.1% accuracy drop | 1 | +| **FP16 → INT8** | 2 $\times$ memory, 2–4 $\times$ throughput | Quantization error | 0.5–1% accuracy drop | 2 | +| **50% Pruning** | ~2 $\times$ smaller model | Capacity loss | 0.5–1% accuracy drop | 2 | +| **Knowledge Distillation** | 2–10 $\times$ smaller student | Capability ceiling | 1–3% accuracy drop | 2 | +| **4-bit Quantization** | 4 $\times$ memory reduction | Significant error | 2–5% accuracy drop | 2–3 | +| **90% Pruning** | ~10 $\times$ smaller model | Severe capacity loss | 5–15% accuracy drop | 3 | +| **↑ Batch Size (8 $\times$)** | Higher throughput, better GPU util | Generalization gap | Requires LR scaling | — | : **The Optimization Tradeoffs.** Region 1 = Free Lunch, Region 2 = Efficient Trade, Region 3 = Danger Zone. Batch size affects training dynamics rather than model quality directly. These ranges are empirical guidelines from published benchmarks [@jacob2018quantization; @han2015deep; @hinton2015distilling]; actual results vary with architecture, task, and implementation quality. {#tbl-optimization-tradeoffs} @@ -579,7 +579,7 @@ Optimization is about trading one resource for another. **Trade-offs** -- [ ] **Accuracy vs. Cost**: Do you understand why removing 50% of weights (Pruning) might drop accuracy by 1%, but reducing precision (Quantization) to INT8 might drop it by 0.5% while saving 4$\times$ memory? +- [ ] **Accuracy vs. Cost**: Do you understand why removing 50% of weights (Pruning) might drop accuracy by 1%, but reducing precision (Quantization) to INT8 might drop it by 0.5% while saving 4 $\times$ memory? - [ ] **The "Free Lunch"**: Can you identify optimizations like **Operator Fusion** that improve speed without hurting accuracy? **Technique Selection** @@ -1107,7 +1107,7 @@ Unstructured pruning does not necessarily improve computational efficiency on mo \index{Pruning!structured} Where unstructured pruning removes individual weights, structured pruning [@li2017pruning][^fn-structured-pruning] eliminates entire computational units: neurons, filters, channels, or layers. This approach produces smaller dense models that map directly to modern machine learning accelerators. Because the resulting architecture remains fully dense, structured pruning leads to more efficient inference on general-purpose hardware than unstructured pruning, which requires specialized execution kernels to exploit its sparse weight matrices. -[^fn-structured-pruning]: **Structured Pruning**: Removing entire filters/channels rather than individual weights, enabling immediate hardware speedups without sparse computation support. ResNet-34 filter pruning achieves 50% FLOP reduction with 1.0% accuracy loss; MobileNetV2 channel pruning yields 3.2$\times$ faster ARM inference at 96.5% accuracy. Importance metrics include magnitude, gradient, and Taylor expansion. +[^fn-structured-pruning]: **Structured Pruning**: Removing entire filters/channels rather than individual weights, enabling immediate hardware speedups without sparse computation support. ResNet-34 filter pruning achieves 50% FLOP reduction with 1.0% accuracy loss; MobileNetV2 channel pruning yields 3.2 $\times$ faster ARM inference at 96.5% accuracy. Importance metrics include magnitude, gradient, and Taylor expansion. Neurons, filters, and layers vary dramatically in their contribution to a model's predictions. Some units primarily carry redundant or low-impact information, and removing them does not significantly degrade model performance. Identifying which structures can be pruned while preserving accuracy remains the core challenge. @@ -1976,7 +1976,7 @@ These trade-offs become concrete when examining real-world deployments. Several [^fn-bert-compression]: **BERT Compression**: BERT-Base (110M params) can be compressed to 67M params (39% reduction) with only 1.2% GLUE score drop. Attention head pruning removes 144 of 192 heads with minimal impact, while layer pruning reduces 12 layers to 6 layers maintaining 97.8% performance. -[^fn-efficientnet-pruning]: **EfficientNet Pruning**: Compound scaling makes EfficientNet amenable to structured pruning. EfficientNet-B0 with 70% pruning maintains 75.8% accuracy (vs. 77.1% baseline), achieving 2.8$\times$ speedup. Channel pruning reduces FLOPs from 390M to 140M, enabling sub-20 ms inference on Pixel 4. Iterative magnitude pruning with fine-tuning preserves accuracy better than one-shot approaches. +[^fn-efficientnet-pruning]: **EfficientNet Pruning**: Compound scaling makes EfficientNet amenable to structured pruning. EfficientNet-B0 with 70% pruning maintains 75.8% accuracy (vs. 77.1% baseline), achieving 2.8 $\times$ speedup. Channel pruning reduces FLOPs from 390M to 140M, enabling sub-20 ms inference on Pixel 4. Iterative magnitude pruning with fine-tuning preserves accuracy better than one-shot approaches. Pruning is powerful but has an inherent limitation: it starts with an existing architecture and carves away pieces. The pruned model inherits its structure from the original—same layer types, same connectivity patterns, just fewer parameters. What if the original architecture itself is inefficient for deployment? What if we want a model with a completely different structure, such as a 6-layer transformer instead of a 12-layer one, that still captures the original model's capabilities? @@ -2263,7 +2263,7 @@ This factorization reveals a fundamental *bandwidth-compute trade-off* that recu ::: {.callout-notebook title="The Bandwidth-Compute Trade-off"} **Reducing the Memory Pressure**: Low-rank factorization illustrates a classic systems trade-off: **trading computation for bandwidth reduction**. Storing a `{python} mat_dim_str` $\times$ `{python} mat_dim_str` matrix requires `{python} full_mb_str` MB (at FP32). Fetching this matrix for a single inference is a massive memory bandwidth hit, especially when limited by physical memory bandwidth constraints. -If we factorize it with rank k = `{python} rank_k_str`, we store two matrices (`{python} mat_dim_str` $\times$ `{python} rank_k_str` and `{python} rank_k_str` $\times$ `{python} mat_dim_str`), totaling only `{python} factored_mb_str` MB, a **`{python} data_reduction_str`$\times$ reduction in data movement**. While the number of floating-point operations (FLOPs) actually *increases* slightly because we perform two smaller matrix multiplies instead of one large one, the system speedup is often dramatic. By reducing data movement by `{python} data_reduction_str`$\times$, we allow the processor to spend more time computing and less time waiting for memory. +If we factorize it with rank k = `{python} rank_k_str`, we store two matrices (`{python} mat_dim_str` $\times$ `{python} rank_k_str` and `{python} rank_k_str` $\times$ `{python} mat_dim_str`), totaling only `{python} factored_mb_str` MB, a **`{python} data_reduction_str` $\times$ reduction in data movement**. While the number of floating-point operations (FLOPs) actually *increases* slightly because we perform two smaller matrix multiplies instead of one large one, the system speedup is often dramatic. By reducing data movement by `{python} data_reduction_str` $\times$, we allow the processor to spend more time computing and less time waiting for memory. ::: This bandwidth-compute trade-off reflects the broader memory wall[^fn-memory-wall-preview] phenomenon where memory access becomes the dominant bottleneck. @@ -2579,7 +2579,7 @@ Pruning, knowledge distillation, and other techniques explored in previous secti The three-stage feedback loop in @fig-nas-flow captures the essence of how NAS works. NAS[^fn-hardware-aware-nas] operates through three interconnected stages: defining the search space (architectural components and constraints), applying search strategies (reinforcement learning[@zoph2017neural], evolutionary algorithms, or gradient-based methods) to explore candidate architectures, and evaluating performance to ensure discovered designs satisfy accuracy and efficiency objectives. The key insight is that this feedback loop allows the search to learn from each evaluation, progressively focusing on promising regions of the architecture space. This automation enables the discovery of novel architectures that often match or surpass human-designed models while requiring substantially less expert effort. -[^fn-hardware-aware-nas]: **Hardware-Aware NAS**: Architecture search [@tan2019mnasnet] directly optimizing for target hardware latency rather than proxy metrics like FLOPs. MnasNet (2019) uses actual measured latency in the search objective, finding architectures with 1.8$\times$ speedup over MobileNetV2 at higher accuracy. Platform-specific search discovers that optimal architectures differ significantly between mobile CPUs, GPUs, and TPUs. +[^fn-hardware-aware-nas]: **Hardware-Aware NAS**: Architecture search [@tan2019mnasnet] directly optimizing for target hardware latency rather than proxy metrics like FLOPs. MnasNet (2019) uses actual measured latency in the search objective, finding architectures with 1.8 $\times$ speedup over MobileNetV2 at higher accuracy. Platform-specific search discovers that optimal architectures differ significantly between mobile CPUs, GPUs, and TPUs. ::: {#fig-nas-flow fig-env="figure" fig-pos="htb" fig-cap="**Neural Architecture Search Flow**: Three components form a feedback loop: a Search Space defines candidate operations, a Search Strategy selects architectures, and a Performance Estimation Strategy evaluates each candidate. The strategy iterates by feeding performance estimates back into the search until convergence." fig-alt="Three-box flowchart showing NAS process. Search Space box feeds into Search Strategy box, which exchanges Architecture and Performance estimate with Performance Estimation Strategy box in a feedback loop."} ```{.tikz} @@ -2618,7 +2618,7 @@ The effectiveness of NAS depends on three design decisions: what architectures t [^fn-rl-nas]: **Reinforcement Learning NAS**: Uses RL controller networks to generate architectures, with accuracy as reward signal. Google's NASNet controller was trained for 22,400 GPU-hours on 800 GPUs, but discovered architectures achieving 82.7% ImageNet accuracy, 28% better than human-designed ResNet at similar FLOP budgets. -[^fn-evolutionary-nas]: **Evolutionary NAS**: Treating architectures as genomes evolved through mutation (adding/removing layers) and crossover (combining parent architectures). AmoebaNet required 3,150 GPU-days achieving 83.9% ImageNet accuracy. Regularized evolution outperformed RL-based NAS in head-to-head comparisons. Modern approaches combine evolution with weight-sharing for 1000$\times$ speedup. +[^fn-evolutionary-nas]: **Evolutionary NAS**: Treating architectures as genomes evolved through mutation (adding/removing layers) and crossover (combining parent architectures). AmoebaNet required 3,150 GPU-days achieving 83.9% ImageNet accuracy. Regularized evolution outperformed RL-based NAS in head-to-head comparisons. Modern approaches combine evolution with weight-sharing for 1000 $\times$ speedup. [^fn-nas-evaluation-metrics]: **NAS Evaluation Metrics**: Multi-objective optimization balancing accuracy, latency, memory, and energy creates Pareto frontiers of non-dominated architectures. Practitioners select architectures based on deployment constraints: edge devices prioritize latency/energy; servers prioritize throughput. Scalarization weights or evolutionary multi-objective methods explore these tradeoffs systematically. @@ -2703,7 +2703,7 @@ Beyond convolutional networks, NAS has been applied to transformer architectures The structural techniques covered so far (pruning, distillation, factorization, and NAS) all optimize *what* computations the model performs: which parameters exist, which connections remain, and how the architecture is structured. These techniques can dramatically reduce parameter counts and theoretical FLOPs. Even a perfectly pruned model with an optimal architecture, however, faces a fundamental constraint: every surviving weight and activation must be stored and processed at some numerical precision. -This brings us to the second dimension of our optimization framework: *how precisely* should those computations be performed? Numerical precision directly determines memory footprint and arithmetic cost, two resources that structural optimization cannot touch. A 32-bit floating-point number uses 4 bytes of memory and requires expensive floating-point arithmetic; an 8-bit integer uses 1 byte and enables fast integer math. For many models, this 4$\times$ reduction in precision translates directly to 4$\times$ reduction in memory bandwidth, and since LLM inference is bandwidth-bound, this means 4$\times$ faster token generation. The accuracy cost is often less than 1%. +This brings us to the second dimension of our optimization framework: *how precisely* should those computations be performed? Numerical precision directly determines memory footprint and arithmetic cost, two resources that structural optimization cannot touch. A 32-bit floating-point number uses 4 bytes of memory and requires expensive floating-point arithmetic; an 8-bit integer uses 1 byte and enables fast integer math. For many models, this 4 $\times$ reduction in precision translates directly to 4 $\times$ reduction in memory bandwidth, and since LLM inference is bandwidth-bound, this means 4 $\times$ faster token generation. The accuracy cost is often less than 1%. Quantization is arguably the single most impactful optimization technique for deployment, especially for large language models. It requires no architectural changes, applies post-training in many cases, and delivers immediate, hardware-agnostic benefits. Before examining the techniques in detail, the following checkpoint tests understanding of the structural optimization methods covered so far. @@ -2733,7 +2733,7 @@ Quantization[^fn-quantization-etymology] affects every neural network weight and [^fn-quantization-etymology]: **Quantization**: From Latin "quantus" (how much), via quantum physics where it describes discrete energy levels. The technique's theoretical foundations trace to Claude Shannon at Bell Labs, whose 1948 paper "A Mathematical Theory of Communication" established the fundamental limits of representing continuous signals with discrete values. In signal processing (1940s-1960s), quantization meant mapping continuous values to discrete levels, introducing "quantization error"—and Shannon proved exactly how much precision you could sacrifice before information was irretrievably lost. ML borrowed both the term and the mathematics directly: converting FP32 weights to INT8 maps continuous values to 256 discrete levels, trading precision for efficiency. When we analyze quantization error in neural networks, we are applying Shannon's six-decade-old framework to a new domain. -This choice directly impacts three system properties. Memory shrinks because an INT8 model is 4$\times$ smaller than FP32, enabling deployment on devices that could never hold the full-precision weights. Bandwidth demand drops proportionally: loading INT8 weights requires 4$\times$ less memory traffic, directly accelerating the bandwidth-bound inference that dominates LLM generation. Compute cost falls as well, since INT8 arithmetic is faster and cheaper than FP32 on most hardware with dedicated low-precision units [@gupta2015deep; @wang2019benchmarking]. +This choice directly impacts three system properties. Memory shrinks because an INT8 model is 4 $\times$ smaller than FP32, enabling deployment on devices that could never hold the full-precision weights. Bandwidth demand drops proportionally: loading INT8 weights requires 4 $\times$ less memory traffic, directly accelerating the bandwidth-bound inference that dominates LLM generation. Compute cost falls as well, since INT8 arithmetic is faster and cheaper than FP32 on most hardware with dedicated low-precision units [@gupta2015deep; @wang2019benchmarking]. The accuracy cost of reduced precision varies by model and technique. CNNs typically tolerate INT8 quantization with <1% accuracy loss; transformers may require more care. This section covers three approaches in increasing complexity: **post-training quantization**\index{Quantization!post-training (PTQ)} (PTQ) for rapid deployment, **quantization-aware training**\index{Quantization!aware training (QAT)} (QAT) for production systems requiring minimal accuracy loss, and **extreme quantization**\index{Quantization!extreme (INT4, binary)} (INT4, binary) for the most constrained environments. @@ -2805,7 +2805,7 @@ Beyond computational and memory benefits, the energy costs associated with diffe \coordinate(S1)at(axis cs:0,27); \coordinate(S2)at(axis cs:2,30); \end{axis} -\node[fill=white,text=red, font=\bfseries\large\usefont{T1}{phv}{m}{n}] at (S2) {100$\times$}; +\node[fill=white,text=red, font=\bfseries\large\usefont{T1}{phv}{m}{n}] at (S2) {100 $\times$}; \draw[red,-latex,line width=2pt](L)--(D); \end{scope} %LEFT @@ -2848,7 +2848,7 @@ These energy savings take on a different character for models where memory capac ::: {.callout-lighthouse title="DLRM and Embedding Quantization"} **The Memory Capacity Constraint**: Our **DLRM Lighthouse** (@sec-network-architectures) presents a unique compression challenge. Unlike ResNet or GPT, which are constrained by compute or bandwidth, DLRM is constrained by **Memory Capacity**. Its embedding tables can reach terabytes in size, far exceeding GPU memory. -For DLRM, quantization is not about faster math; it's about **storage density**. Quantizing embedding tables from FP32 to INT8 (or INT4) reduces memory footprint by 4–8$\times$, allowing larger tables to fit on fewer GPUs. This is a pure **Information Density** optimization: we compress the lookup table so the **Machine** (Physics) can hold the **Algorithm** (Logic). +For DLRM, quantization is not about faster math; it's about **storage density**. Quantizing embedding tables from FP32 to INT8 (or INT4) reduces memory footprint by 4–8 $\times$, allowing larger tables to fit on fewer GPUs. This is a pure **Information Density** optimization: we compress the lookup table so the **Machine** (Physics) can hold the **Algorithm** (Logic). ::: \index{Keyword Spotting (KWS)!quantization imperative} @@ -2857,12 +2857,12 @@ While DLRM operates at the terabyte scale, our Smart Doorbell Lighthouse faces t ::: {.callout-lighthouse title="The TinyML Quantization Imperative"} **The Energy and Storage Constraint**: Our **Smart Doorbell Lighthouse** operates at the opposite extreme of the Iron Law from DLRM. While DLRM optimizes for terabyte-scale capacity, the Smart Doorbell's Keyword Spotting (KWS) model must operate within a 100 KB budget to run on a microcontroller with 256 KB RAM. -In FP32, even the compact DS-CNN architecture consumes 4$\times$ more memory bandwidth and energy per inference than in INT8. For an always-on device running on a coin cell battery, this 4$\times$ energy difference translates directly to battery life: a device that lasts 1 month on FP32 might last 4 months on INT8. Here, quantization is the primary lever for the **Energy Term** ($O / (R_{peak} \cdot \eta)$) of the Iron Law. +In FP32, even the compact DS-CNN architecture consumes 4 $\times$ more memory bandwidth and energy per inference than in INT8. For an always-on device running on a coin cell battery, this 4 $\times$ energy difference translates directly to battery life: a device that lasts 1 month on FP32 might last 4 months on INT8. Here, quantization is the primary lever for the **Energy Term** ($O / (R_{peak} \cdot \eta)$) of the Iron Law. ::: Beyond direct compute savings, reducing numerical precision has a significant impact on memory energy consumption, which often dominates total system power. Lower-precision representations reduce data storage requirements and memory bandwidth usage, leading to fewer and more efficient memory accesses. Accessing memory, particularly off-chip DRAM, is far more energy-intensive than performing arithmetic operations: DRAM accesses require orders of magnitude more energy (1.3-2.6 nJ) compared to cache accesses (e.g., 10 pJ for an 8 KB L1 cache access). An instruction's total energy can therefore be dominated by memory access patterns rather than computation[^fn-energy-efficiency-metrics]. -[^fn-energy-efficiency-metrics]: **Energy Efficiency Metrics**: INT8 quantization reduces energy consumption by 4–8$\times$ over FP32 on supported hardware. MobileNetV2 INT8 consumes `{python} mobilenet_int8_mj_str`mJ vs. `{python} mobilenet_fp32_mj_str`mJ FP32 per inference on Cortex-A75. ResNet-50 on TPU v4 achieves `{python} tpu_v4_tops_per_w_str` TOPS/Watt vs. `{python} v100_tops_per_w_str` TOPS/Watt on V100 GPU. +[^fn-energy-efficiency-metrics]: **Energy Efficiency Metrics**: INT8 quantization reduces energy consumption by 4–8 $\times$ over FP32 on supported hardware. MobileNetV2 INT8 consumes `{python} mobilenet_int8_mj_str`mJ vs. `{python} mobilenet_fp32_mj_str`mJ FP32 per inference on Cortex-A75. ResNet-50 on TPU v4 achieves `{python} tpu_v4_tops_per_w_str` TOPS/Watt vs. `{python} v100_tops_per_w_str` TOPS/Watt on V100 GPU. Reducing numerical precision thus improves efficiency on two fronts: faster computation and less data movement. This dual benefit is especially valuable for hardware accelerators and edge devices, where memory bandwidth and power efficiency are binding constraints. @@ -3028,7 +3028,7 @@ compression_ratio_str = QuantizationSavings.compression_ratio_str - **Size**: `{python} llm_params_b_str` $\times 10^9$ $\times$ `{python} int4_bytes_str` bytes (4-bit) = `{python} int4_size_gb_str` GB - **Hardware Req**: Fits comfortably on 8GB GPU (e.g., T4, consumer laptops). -**Impact**: `{python} compression_ratio_str`$\times$ compression allows deployment on commodity hardware, reducing cost by 5–10$\times$. +**Impact**: `{python} compression_ratio_str` $\times$ compression allows deployment on commodity hardware, reducing cost by 5–10 $\times$. ::: Beyond storage savings, quantization also accelerates computation through hardware parallelism. The speedup is not merely about arithmetic being cheaper—it emerges from how modern processors pack more operations into the same hardware resources when working with smaller data types. The following calculation illustrates the impact of *the SIMD multiplier*. @@ -3102,10 +3102,10 @@ simd_int8_bits_str = SIMDThroughput.simd_int8_bits_str 2. **FP32 Capacity**: `{python} simd_register_bits_str` / `{python} simd_fp32_bits_str` = `{python} simd_fp32_str` operations per cycle. 3. **INT8 Capacity**: `{python} simd_register_bits_str` / `{python} simd_int8_bits_str` = `{python} simd_int8_str` operations per cycle. -**The Multiplier**: By switching to INT8, you pack **`{python} simd_gain_str`$\times$ more elements** into the same register. -Throughput Gain = INT8 Ops/Cycle / FP32 Ops/Cycle = `{python} simd_int8_str` / `{python} simd_fp32_str` = `{python} simd_gain_str`$\times$ +**The Multiplier**: By switching to INT8, you pack **`{python} simd_gain_str` $\times$ more elements** into the same register. +Throughput Gain = INT8 Ops/Cycle / FP32 Ops/Cycle = `{python} simd_int8_str` / `{python} simd_fp32_str` = `{python} simd_gain_str` $\times$ -**The Systems Conclusion**: Quantization delivers a **`{python} simd_gain_str`$\times$ speedup** on compute-bound layers solely due to vector packing, even before considering memory bandwidth savings. +**The Systems Conclusion**: Quantization delivers a **`{python} simd_gain_str` $\times$ speedup** on compute-bound layers solely due to vector packing, even before considering memory bandwidth savings. ::: Reducing numerical precision introduces trade-offs, however. Lower-precision formats can cause numerical instability and quantization noise, potentially affecting model accuracy. Some architectures, such as large transformer-based NLP models, tolerate quantization well, whereas others may experience significant degradation. Selecting the appropriate numerical precision therefore requires balancing accuracy constraints, hardware support, and efficiency gains. @@ -3119,27 +3119,27 @@ To appreciate how precision loss manifests in practice, examine the representati **The Math**: On modern hardware with dedicated INT8 units: -1. **Tensor Core Throughput**: NVIDIA A100 delivers `{python} a100_tflops_fp16_str` TFLOPS for FP16 vs `{python} a100_tflops_int8_str` TOPS for INT8, a `{python} a100_int8_speedup_str`$\times$ peak throughput increase. +1. **Tensor Core Throughput**: NVIDIA A100 delivers `{python} a100_tflops_fp16_str` TFLOPS for FP16 vs `{python} a100_tflops_int8_str` TOPS for INT8, a `{python} a100_int8_speedup_str` $\times$ peak throughput increase. 2. **Memory Bandwidth**: INT8 weights are half the size, so loading them from memory takes half the time. -3. **Combined Effect**: For compute-bound operations, the speedup is primarily from compute throughput: **~`{python} a100_int8_speedup_str`$\times$ speedup**. +3. **Combined Effect**: For compute-bound operations, the speedup is primarily from compute throughput: **~`{python} a100_int8_speedup_str` $\times$ speedup**. -**The Systems Insight**: The speedup from quantization depends on the bottleneck. Compute-bound operations (large batch sizes, high arithmetic intensity) see ~`{python} a100_int8_speedup_str`$\times$ from faster INT8 units. The bandwidth-bound case (demonstrated in the Optimization Framework's napkin math) achieves up to `{python} bandwidth_bound_speedup_str`$\times$ because memory traffic dominates, so halving data size nearly doubles effective throughput for both bandwidth and compute. +**The Systems Insight**: The speedup from quantization depends on the bottleneck. Compute-bound operations (large batch sizes, high arithmetic intensity) see ~`{python} a100_int8_speedup_str` $\times$ from faster INT8 units. The bandwidth-bound case (demonstrated in the Optimization Framework's napkin math) achieves up to `{python} bandwidth_bound_speedup_str` $\times$ because memory traffic dominates, so halving data size nearly doubles effective throughput for both bandwidth and compute. ::: ### Numerical Format Comparison {#sec-model-compression-numerical-format-comparison-8fa2} @tbl-numerics compares commonly used numerical precision formats in machine learning, each exhibiting distinct trade-offs in storage efficiency, computational speed, and energy consumption. Emerging formats like FP8\index{FP8} and TF32\index{TF32 (TensorFloat-32)} have been introduced to further optimize performance, especially on AI accelerators. -| **Precision Format** | **Bit-Width** | **Storage Reduction (vs FP32)** | **Compute Speed (vs FP32)** | **Power Consumption** | **Use Cases** | -|:-------------------------------------------|--------------:|--------------------------------:|---------------------------------------------:|:----------------------|:------------------------------------------------------------| -| **FP32 (Single-Precision Floating Point)** | 32-bit | Baseline (1$\times$) | Baseline (1$\times$) | High | Training & inference (general-purpose) | -| **FP16 (Half-Precision Floating Point)** | 16-bit | 2$\times$ smaller | 2$\times$ faster on FP16-optimized hardware | Lower | Accelerated training, inference (NVIDIA Tensor Cores, TPUs) | -| **bfloat16 (Brain Floating Point)** | 16-bit | 2$\times$ smaller | Similar speed to FP16, better dynamic range | Lower | Training on TPUs, transformer-based models | -| **TF32 (TensorFloat-32)** | 19-bit | Similar to FP16 | Up to 8$\times$ faster on NVIDIA Ampere GPUs | Lower | Training on NVIDIA GPUs | -| **FP8 (Floating-Point 8-bit)** | 8-bit | 4$\times$ smaller | Faster than INT8 in some cases | Significantly lower | Efficient training/inference (H100, AI accelerators) | -| **INT8 (8-bit Integer)** | 8-bit | 4$\times$ smaller | 4–8$\times$ faster than FP32 | Significantly lower | Quantized inference (Edge AI, mobile AI, NPUs) | -| **INT4 (4-bit Integer)** | 4-bit | 8$\times$ smaller | Hardware-dependent | Extremely low | Ultra-low-power AI, experimental quantization | -| **Binary/Ternary (1-bit / 2-bit)** | 1–2-bit | 16–32$\times$ smaller | Highly hardware-dependent | Lowest | Extreme efficiency (binary/ternary neural networks) | +| **Precision Format** | **Bit-Width** | **Storage Reduction (vs FP32)** | **Compute Speed (vs FP32)** | **Power Consumption** | **Use Cases** | +|:-------------------------------------------|--------------:|--------------------------------:|----------------------------------------------:|:----------------------|:------------------------------------------------------------| +| **FP32 (Single-Precision Floating Point)** | 32-bit | Baseline (1 $\times$) | Baseline (1 $\times$) | High | Training & inference (general-purpose) | +| **FP16 (Half-Precision Floating Point)** | 16-bit | 2 $\times$ smaller | 2 $\times$ faster on FP16-optimized hardware | Lower | Accelerated training, inference (NVIDIA Tensor Cores, TPUs) | +| **bfloat16 (Brain Floating Point)** | 16-bit | 2 $\times$ smaller | Similar speed to FP16, better dynamic range | Lower | Training on TPUs, transformer-based models | +| **TF32 (TensorFloat-32)** | 19-bit | Similar to FP16 | Up to 8 $\times$ faster on NVIDIA Ampere GPUs | Lower | Training on NVIDIA GPUs | +| **FP8 (Floating-Point 8-bit)** | 8-bit | 4 $\times$ smaller | Faster than INT8 in some cases | Significantly lower | Efficient training/inference (H100, AI accelerators) | +| **INT8 (8-bit Integer)** | 8-bit | 4 $\times$ smaller | 4–8 $\times$ faster than FP32 | Significantly lower | Quantized inference (Edge AI, mobile AI, NPUs) | +| **INT4 (4-bit Integer)** | 4-bit | 8 $\times$ smaller | Hardware-dependent | Extremely low | Ultra-low-power AI, experimental quantization | +| **Binary/Ternary (1-bit / 2-bit)** | 1–2-bit | 16–32 $\times$ smaller | Highly hardware-dependent | Lowest | Extreme efficiency (binary/ternary neural networks) | : **Numerical Precision Formats**: Comparison of precision formats by bit width, memory reduction, computational efficiency, accuracy retention, and typical use cases across deployment contexts. {#tbl-numerics} @@ -3221,7 +3221,7 @@ node [midway,above=5mm] {\textbf{16 bits}}; ``` ::: -INT8\index{INT8!efficiency gains}\index{Quantization!INT8 (8-bit integer)} precision offers more aggressive efficiency improvements for inference workloads. Many quantized models use INT8 for inference, reducing storage by $4\times$ while accelerating computation by 4–8$\times$ on optimized hardware. INT8 is widely used in mobile and embedded AI, where energy constraints are significant. As the quantization savings calculation demonstrated earlier, this $4\times$ reduction in model size enables a `{python} llm_params_b_str` billion parameter model to fit on a single consumer GPU rather than requiring data center hardware. +INT8\index{INT8!efficiency gains}\index{Quantization!INT8 (8-bit integer)} precision offers more aggressive efficiency improvements for inference workloads. Many quantized models use INT8 for inference, reducing storage by $4\times$ while accelerating computation by 4–8 $\times$ on optimized hardware. INT8 is widely used in mobile and embedded AI, where energy constraints are significant. As the quantization savings calculation demonstrated earlier, this $4\times$ reduction in model size enables a `{python} llm_params_b_str` billion parameter model to fit on a single consumer GPU rather than requiring data center hardware. Binary and ternary networks\index{Quantization!binary networks}\index{Quantization!ternary networks} represent the extreme end of quantization, where weights and activations are constrained to 1-bit (binary) or 2-bit (ternary) values. This results in massive storage and energy savings, but model accuracy often degrades significantly unless specialized architectures are used. Our *keyword spotting* lighthouse lives precisely in this regime, necessitating strategies for *keyword spotting and extreme compression*. @@ -3235,7 +3235,7 @@ For KWS, INT8 quantization is often just the *starting point*. To fit complex ac \index{Energy Efficiency!quantization benefits} \index{Model Compression!energy efficiency} -Quantization reduces energy consumption through two complementary mechanisms: smaller data types require fewer memory accesses (the dominant energy cost, as established in the Physics of Quantization analysis above), and lower-precision arithmetic units consume less power per operation. An INT8 operation, for example, uses roughly `{python} int8_fp32_energy_ratio_str`$\times$ less energy than its FP32 equivalent, compounding the memory energy savings into substantial reductions at system scale. +Quantization reduces energy consumption through two complementary mechanisms: smaller data types require fewer memory accesses (the dominant energy cost, as established in the Physics of Quantization analysis above), and lower-precision arithmetic units consume less power per operation. An INT8 operation, for example, uses roughly `{python} int8_fp32_energy_ratio_str` $\times$ less energy than its FP32 equivalent, compounding the memory energy savings into substantial reductions at system scale. These gains depend on hardware support. AI accelerators with dedicated low-precision units (Tensor Cores for FP16/INT8, or the newer FP8 units on H100-class hardware) realize the full energy benefit, while general-purpose CPUs lacking such units see limited improvement. The practical implication is that energy efficiency from quantization is not a software-only optimization; it requires matching the chosen precision format to the target hardware's arithmetic capabilities. @@ -3456,9 +3456,9 @@ where: - $x$ is the original floating-point value, - $s$ is a scaling factor that maps the floating-point range to the available integer range. -@lst-quantization_example demonstrates uniform quantization from FP32 to INT8, achieving 4$\times$ memory reduction while measuring the resulting quantization error. +@lst-quantization_example demonstrates uniform quantization from FP32 to INT8, achieving 4 $\times$ memory reduction while measuring the resulting quantization error. -::: {#lst-quantization_example lst-cap="**Uniform Quantization**: Converts FP32 weights to INT8 format, achieving 4$\times$ memory reduction while measuring quantization error."} +::: {#lst-quantization_example lst-cap="**Uniform Quantization**: Converts FP32 weights to INT8 format, achieving 4 $\times$ memory reduction while measuring quantization error."} ```{.python} import torch @@ -4010,7 +4010,7 @@ Activation Quantization refers to the process of quantizing the activation value \index{LLaMA!AWQ quantization} Recent advancements have explored **Activation-aware Weight Quantization (AWQ)**\index{Quantization!activation-aware weight (AWQ)}[^fn-awq] for the compression and acceleration of large language models (LLMs). This approach is particularly relevant for our **GPT-2 / Llama Lighthouse**, which is memory-bandwidth bound. By protecting only a small fraction of the most salient weights (approximately 1%) based on activation magnitude, AWQ enables effective 4-bit weight quantization. This reduces the memory traffic required to load the massive parameter set for every token generation, directly attacking the primary bottleneck of generative inference [@lin2023awq]. -[^fn-awq]: **Activation-aware Weight Quantization (AWQ)**: Observes that only approximately 1% of weights disproportionately affect accuracy based on activation patterns. By protecting these salient weights while aggressively quantizing others, AWQ achieves INT4 quantization of LLaMA-7B with <1% perplexity degradation. Delivers 3.2$\times$ speedup on A100 GPUs by reducing memory bandwidth requirements from 14GB to 3.5GB per inference. +[^fn-awq]: **Activation-aware Weight Quantization (AWQ)**: Observes that only approximately 1% of weights disproportionately affect accuracy based on activation patterns. By protecting these salient weights while aggressively quantizing others, AWQ achieves INT4 quantization of LLaMA-7B with <1% perplexity degradation. Delivers 3.2 $\times$ speedup on A100 GPUs by reducing memory bandwidth requirements from 14GB to 3.5GB per inference. ##### Static vs. Dynamic Quantization {#sec-model-compression-static-vs-dynamic-quantization-4791} @@ -4266,7 +4266,7 @@ The following checkpoint tests understanding of quantization concepts before pro Test your understanding of quantization before moving to architectural efficiency: -- [ ] Can you explain why INT8 quantization provides roughly 4$\times$ memory reduction but potentially more than 4$\times$ energy reduction? Consider the energy cost of floating-point versus integer arithmetic units. +- [ ] Can you explain why INT8 quantization provides roughly 4 $\times$ memory reduction but potentially more than 4 $\times$ energy reduction? Consider the energy cost of floating-point versus integer arithmetic units. - [ ] Do you understand the key advantage of quantization-aware training (QAT) over post-training quantization (PTQ), and when the extra training cost is justified? - [ ] Can you explain why weight quantization provides nearly linear speedup with bit-width reduction for memory-bandwidth-bound LLM inference? Think about which resource is the bottleneck during autoregressive generation. ::: @@ -4302,7 +4302,7 @@ Designing for hardware efficiency requires structuring architectures to account The principles in @tbl-hardware-efficient-design work synergistically: scaling optimization sizes models appropriately for available resources, computation reduction eliminates redundant operations through techniques like depthwise separable convolutions[^fn-depthwise-separable-efficiency], memory optimization aligns access patterns with hardware hierarchies, and hardware-aware design ensures architectural decisions match platform capabilities. Together, these principles enable models that balance accuracy with efficiency while maintaining consistent behavior across deployment environments. -[^fn-depthwise-separable-efficiency]: **Depthwise Separable Convolutions**: Factorizes standard convolution into depthwise (per-channel) and pointwise (1 $\times$ 1) operations, reducing computation by 8–9$\times$. MobileNetV2 achieves 72% ImageNet accuracy with `{python} mobilenetv2_mflops_str` M FLOPs vs. ResNet-50's 76% with `{python} resnet_gflops_str` B FLOPs (13.7$\times$ fewer operations). Enables real-time inference on mobile devices. +[^fn-depthwise-separable-efficiency]: **Depthwise Separable Convolutions**: Factorizes standard convolution into depthwise (per-channel) and pointwise (1 $\times$ 1) operations, reducing computation by 8–9 $\times$. MobileNetV2 achieves 72% ImageNet accuracy with `{python} mobilenetv2_mflops_str` M FLOPs vs. ResNet-50's 76% with `{python} resnet_gflops_str` B FLOPs (13.7 $\times$ fewer operations). Enables real-time inference on mobile devices. The following subsections examine each principle in detail, beginning with how to scale model dimensions effectively. @@ -4333,7 +4333,7 @@ This principle extends beyond convolutional models to other architectures like t Modern architectures leverage factorized computations to decompose complex operations into simpler components, reducing computational overhead while maintaining representational power. Standard convolutions apply filters uniformly across all spatial locations and channels, creating computational bottlenecks on resource-constrained hardware. Factorization techniques address this inefficiency by restructuring operations to minimize redundant computation. -Depthwise separable convolutions\index{Depthwise Separable Convolution}\index{Model Compression!depthwise separable convolution}, introduced in MobileNet, exemplify this approach by decomposing standard convolutions into two stages: depthwise convolution (applying separate filters to each input channel independently) and pointwise convolution (1$\times$1 convolution mixing outputs across channels). The computational complexity of standard convolution with input size $h \times w$, $C_{\text{in}}$ input channels, and $C_{\text{out}}$ output channels is: +Depthwise separable convolutions\index{Depthwise Separable Convolution}\index{Model Compression!depthwise separable convolution}, introduced in MobileNet, exemplify this approach by decomposing standard convolutions into two stages: depthwise convolution (applying separate filters to each input channel independently) and pointwise convolution (1 $\times$ 1 convolution mixing outputs across channels). The computational complexity of standard convolution with input size $h \times w$, $C_{\text{in}}$ input channels, and $C_{\text{out}}$ output channels is: $$ \mathcal{O}(h w C_{\text{in}} C_{\text{out}} k^2) $$ @@ -4341,12 +4341,12 @@ where $k$ is kernel size. Depthwise separable convolutions reduce this to: $$ \mathcal{O}(h w C_{\text{in}} k^2) + \mathcal{O}(h w C_{\text{in}} C_{\text{out}}) $$ -eliminating the $k^2$ factor from channel-mixing operations, achieving 5$\times$-10$\times$ FLOP reduction. This directly translates to reduced memory bandwidth requirements and improved inference latency on mobile and edge devices. +eliminating the $k^2$ factor from channel-mixing operations, achieving 5 $\times$-10 $\times$ FLOP reduction. This directly translates to reduced memory bandwidth requirements and improved inference latency on mobile and edge devices. \index{Grouped Convolution!ResNeXt} \index{Bottleneck Layers!ResNet dimensionality reduction} \index{SqueezeNet!1x1 convolution parameter reduction} -Complementary factorization techniques extend these benefits. Grouped convolutions (ResNeXt) partition feature maps into independent groups processed separately before merging, maintaining accuracy while reducing redundant operations. Bottleneck layers (ResNet) apply 1$\times$1 convolutions to reduce feature dimensionality before expensive operations, concentrating computation where it provides maximum value. Combined with sparsity and hardware-aware scheduling, these techniques maximize accelerator utilization across GPUs, TPUs, and specialized edge processors. +Complementary factorization techniques extend these benefits. Grouped convolutions (ResNeXt) partition feature maps into independent groups processed separately before merging, maintaining accuracy while reducing redundant operations. Bottleneck layers (ResNet) apply 1 $\times$ 1 convolutions to reduce feature dimensionality before expensive operations, concentrating computation where it provides maximum value. Combined with sparsity and hardware-aware scheduling, these techniques maximize accelerator utilization across GPUs, TPUs, and specialized edge processors. While reducing computation is essential, memory constraints often prove more limiting than compute capacity on resource-constrained devices. The next section addresses these memory bottlenecks directly. @@ -4354,7 +4354,7 @@ While reducing computation is essential, memory constraints often prove more lim Memory optimization[^fn-memory-optimization] addresses performance bottlenecks arising when memory demands for activations, feature maps, and parameters exceed hardware capacity on resource-constrained devices. Modern architectures employ memory-efficient strategies to reduce storage requirements while maintaining performance, ensuring computational tractability and energy efficiency on GPUs, TPUs, and edge AI platforms. -[^fn-memory-optimization]: **Memory Optimization**: Techniques reducing peak memory during training and inference. DenseNet-121's feature reuse significantly reduces activation memory compared to ResNet-50; gradient checkpointing typically trades 15–25% additional compute for substantial memory reduction (up to 10$\times$ for some architectures). In-place operations, memory pooling, and operator fusion further reduce footprint, enabling LLM inference on consumer GPUs. +[^fn-memory-optimization]: **Memory Optimization**: Techniques reducing peak memory during training and inference. DenseNet-121's feature reuse significantly reduces activation memory compared to ResNet-50; gradient checkpointing typically trades 15–25% additional compute for substantial memory reduction (up to 10 $\times$ for some architectures). In-place operations, memory pooling, and operator fusion further reduce footprint, enabling LLM inference on consumer GPUs. \index{DenseNet!feature reuse} One effective technique is feature reuse\index{Memory Optimization!feature reuse}, employed in DenseNet. In traditional convolutional networks, each layer computes a new set of feature maps, increasing the model's memory footprint. DenseNet reduces redundant activations by reusing feature maps from previous layers and selectively applying transformations, lowering memory requirements without sacrificing accuracy. In a standard convolutional network with $N_L$ layers, if each layer generates $k$ new feature maps, the total number of feature maps grows linearly: @@ -4385,9 +4385,9 @@ $$ \mathcal{O}(C_{\text{in}} C_{\text{out}} k^2) $$ -By reducing $C_{\text{in}}$ using $1\times 1$ convolutions, SqueezeNet[^fn-squeezenet] reduces the number of parameters, achieving a 50$\times$ reduction in parameter count compared to AlexNet while maintaining similar performance. This method is well-suited for edge devices that have strict memory and storage constraints. +By reducing $C_{\text{in}}$ using $1\times 1$ convolutions, SqueezeNet[^fn-squeezenet] reduces the number of parameters, achieving a 50 $\times$ reduction in parameter count compared to AlexNet while maintaining similar performance. This method is well-suited for edge devices that have strict memory and storage constraints. -[^fn-squeezenet]: **SqueezeNet**: DeepScale/Berkeley architecture using fire modules (squeeze + expand layers) achieves AlexNet-level accuracy (57.5% top-1 ImageNet) with 50$\times$ fewer parameters (1.25M vs 60M). Model size drops from 240MB to ~5MB uncompressed, enabling deployment on smartphones and embedded systems with limited storage. +[^fn-squeezenet]: **SqueezeNet**: DeepScale/Berkeley architecture using fire modules (squeeze + expand layers) achieves AlexNet-level accuracy (57.5% top-1 ImageNet) with 50 $\times$ fewer parameters (1.25M vs 60M). Model size drops from 240MB to ~5MB uncompressed, enabling deployment on smartphones and embedded systems with limited storage. Feature reuse, activation checkpointing, and parameter reduction form key components of hardware-aware model design, allowing models to fit within memory limits of modern accelerators while reducing power consumption through fewer memory accesses. Specialized accelerators like TPUs and GPUs leverage memory hierarchies, caching, and high bandwidth memory to efficiently handle sparse or reduced-memory representations, enabling faster inference with minimal overhead. @@ -4568,7 +4568,7 @@ transfer_reduction_str = fmt(transfer_reduction_value, precision=0, commas=False conv_bn_relu_mem_md = md_math(f"2 \\times 256 \\times 28 \\times 28 \\times 4 \\text{{ bytes}} \\approx \\text{{{conv_bn_relu_intermediate_mb_str} MB}}") ``` -The arithmetic operations remain identical, but memory traffic drops from 6 transfers to 2 transfers (`{python} transfer_reduction_str`$\times$ reduction). For a ResNet-50 layer with 256 channels and spatial size $28 \times 28$, this eliminates `{python} conv_bn_relu_mem_md` of intermediate memory traffic per layer. +The arithmetic operations remain identical, but memory traffic drops from 6 transfers to 2 transfers (`{python} transfer_reduction_str` $\times$ reduction). For a ResNet-50 layer with 256 channels and spatial size $28 \times 28$, this eliminates `{python} conv_bn_relu_mem_md` of intermediate memory traffic per layer. ##### Element-wise Operation Fusion {.unnumbered} @@ -4623,7 +4623,7 @@ saved_us_str = fmt(saved_us_value, precision=0, commas=False) gemm_mem_md = md_math(f"768 \\times 512 \\times 4 \\text{{ bytes}} = \\text{{{gemm_intermediate_mb_str} MB}}") ``` -For tensors of size 1 MB, unfused execution performs 8 MB of memory traffic. Fused execution reduces this to 3 MB (`{python} ew_reduction_str`$\times$ reduction). On a GPU with `{python} v100_bw_gbs_str` GB/s memory bandwidth, this saves approximately `{python} saved_us_str` microseconds per operation, accumulating to milliseconds of latency reduction over thousands of operations in a full model. +For tensors of size 1 MB, unfused execution performs 8 MB of memory traffic. Fused execution reduces this to 3 MB (`{python} ew_reduction_str` $\times$ reduction). On a GPU with `{python} v100_bw_gbs_str` GB/s memory bandwidth, this saves approximately `{python} saved_us_str` microseconds per operation, accumulating to milliseconds of latency reduction over thousands of operations in a full model. ##### Matrix Multiply-Add Fusion {.unnumbered} @@ -4662,13 +4662,13 @@ output = linear(attn_output) ``` ::: -This fusion pattern[^fn-flashattention-compression] achieves 2–4$\times$ speedup on long sequences by dramatically reducing memory bandwidth requirements—a technique central to enabling long-context transformers, as detailed in @sec-model-training-flash-attention-ioaware-attention-optimization-3da0. +This fusion pattern[^fn-flashattention-compression] achieves 2–4 $\times$ speedup on long sequences by dramatically reducing memory bandwidth requirements—a technique central to enabling long-context transformers, as detailed in @sec-model-training-flash-attention-ioaware-attention-optimization-3da0. -[^fn-flashattention-compression]: FlashAttention\index{FlashAttention} (introduced in @sec-network-architectures) demonstrates fusion's power for memory-bound attention: tiling to SRAM yields 2–4$\times$ speedup and enables 64K context windows by reducing memory traffic from O(n^2) to O(n). This exemplifies how operator fusion can transform memory-bound bottlenecks into compute-bound operations. +[^fn-flashattention-compression]: FlashAttention\index{FlashAttention} (introduced in @sec-network-architectures) demonstrates fusion's power for memory-bound attention: tiling to SRAM yields 2–4 $\times$ speedup and enables 64K context windows by reducing memory traffic from O(n^2) to O(n). This exemplifies how operator fusion can transform memory-bound bottlenecks into compute-bound operations. Memory bandwidth analysis quantifies these fusion benefits concretely. Consider a Conv-BN-ReLU sequence operating on a $28 \times 28 \times 256$ feature map (`{python} feat_map_kb_str` KB). Without fusion, each operation performs its own memory round-trip: Conv reads input (`{python} feat_map_kb_str` KB) plus weights (`{python} weights_mb_str` MB) and writes output (`{python} feat_map_kb_str` KB), totaling `{python} unfused_conv_mb_str` MB. BN then reads that output, adds its parameters (`{python} bn_params_kb_str` KB), and writes again, for `{python} unfused_bn_mb_str` MB. ReLU repeats the pattern for another `{python} unfused_relu_mb_str` MB. The total unfused memory traffic is `{python} total_unfused_mb_str` MB. With fusion, the entire sequence reads input and weights once and writes the final output once, requiring only `{python} total_fused_mb_str` MB — a `{python} bandwidth_reduction_pct_str`% bandwidth reduction. -On a V100 GPU with `{python} v100_bw_gbs_str` GB/s HBM bandwidth and `{python} v100_tflops_fp32_str` TFLOPS FP32 compute, the unfused sequence takes approximately `{python} unfused_time_us_str` microseconds (memory-bound), while the fused version takes approximately `{python} fused_time_us_str` microseconds (`{python} fusion_speedup_str`$\times$ speedup). The speedup comes entirely from reducing memory traffic, as the compute remains identical. +On a V100 GPU with `{python} v100_bw_gbs_str` GB/s HBM bandwidth and `{python} v100_tflops_fp32_str` TFLOPS FP32 compute, the unfused sequence takes approximately `{python} unfused_time_us_str` microseconds (memory-bound), while the fused version takes approximately `{python} fused_time_us_str` microseconds (`{python} fusion_speedup_str` $\times$ speedup). The speedup comes entirely from reducing memory traffic, as the compute remains identical. Operator fusion implementation occurs at multiple levels in the software stack: @@ -4703,10 +4703,10 @@ def identify_fusible_patterns(graph): Fusion effectiveness varies by workload characteristics. Memory-bound operations benefit most from fusion, while compute-bound operations see minimal improvement. Quantitative analysis shows: -- **Element-wise operations**: 2–4$\times$ speedup (highly memory-bound, low arithmetic intensity) -- **Conv-BN-Act patterns**: 1.5–2$\times$ speedup (mixed memory/compute characteristics) -- **GEMM-based operations**: 1.2–1.5$\times$ speedup (compute-bound, fusion reduces memory-bound tail) -- **Attention mechanisms**: 2–4$\times$ speedup on long sequences (quadratic memory scaling) +- **Element-wise operations**: 2–4 $\times$ speedup (highly memory-bound, low arithmetic intensity) +- **Conv-BN-Act patterns**: 1.5–2 $\times$ speedup (mixed memory/compute characteristics) +- **GEMM-based operations**: 1.2–1.5 $\times$ speedup (compute-bound, fusion reduces memory-bound tail) +- **Attention mechanisms**: 2–4 $\times$ speedup on long sequences (quadratic memory scaling) Fusion also reduces kernel launch overhead. Each CUDA kernel launch incurs approximately 5-10 microseconds of latency. For a ResNet-50 with 53 convolutional layers, unfused execution launches `{python} kernels_unfused_str` kernels (Conv + BN + ReLU), while fused execution launches `{python} kernels_fused_str` kernels, saving approximately `{python} saved_latency_ms_str` millisecond from launch overhead alone. @@ -4830,7 +4830,7 @@ A well-known example of this paradigm is the Dynamic Filter Network (DFN)\index{ Another widely adopted strategy is the Mixture of Experts (MoE)\index{Mixture of Experts (MoE)!definition} framework. The key insight is that different inputs may benefit from different types of processing. A question about mathematics and a question about history might best be handled by different "specialist" subnetworks. In this architecture, a gating network\index{Mixture of Experts (MoE)!gating network} selects a subset of specialized expert subnetworks to process each input [@shazeer2017outrageously]. This allows only a small portion of the total model to be active for any given input, significantly improving computational efficiency without sacrificing model capacity. A notable instantiation of this idea is Google's Switch Transformer\index{Switch Transformer}\index{Mixture of Experts (MoE)!Switch Transformer}[^fn-switch-transformer], which extends the transformer architecture with expert-based conditional computation [@fedus2021switch]. While we introduce MoE principles here for single-system context, large-scale MoE deployments involving distributed expert placement are explored in advanced coverage of large-scale systems. -[^fn-switch-transformer]: **Switch Transformer**: Scales to 1.6 trillion parameters while activating only 2 billion per token, achieving 7$\times$ faster pretraining than dense T5 at equivalent FLOPs. Routes each token to a single expert (vs. top-k), reducing communication overhead. Training instability from load imbalance requires auxiliary loss terms; production deployments use capacity factors of 1.25–2$\times$ to handle routing variance. +[^fn-switch-transformer]: **Switch Transformer**: Scales to 1.6 trillion parameters while activating only 2 billion per token, achieving 7 $\times$ faster pretraining than dense T5 at equivalent FLOPs. Routes each token to a single expert (vs. top-k), reducing communication overhead. Training instability from load imbalance requires auxiliary loss terms; production deployments use capacity factors of 1.25–2 $\times$ to handle routing variance. Look at the right side of @fig-switch-transformer to see this routing in action: the Switch Transformer replaces the traditional feedforward layer with a Switching FFN Layer. For each token, a lightweight router selects a single expert from a pool of feedforward networks. The router outputs a probability distribution over available experts, and the highest-probability expert is activated per token. This design enables large models to scale parameter count without proportionally increasing inference cost. @@ -6312,7 +6312,7 @@ yshift=0mm,fill=none,fit=(cell-1-1DM3),line width=3.5pt](BB3){}; Modern hardware accelerators provide specialized support for sparse operations, though the degree of acceleration depends on sparsity structure and hardware capabilities. @sec-hardware-acceleration examines these accelerators in depth; here we summarize their sparsity-specific features. \index{Sparse Tensor Cores} -GPUs\index{Tensor Cores!sparse acceleration}\index{Hardware-Aware Optimization!sparse tensor cores} with Sparse Tensor Cores (NVIDIA Ampere and later) accelerate structured sparsity patterns like 2:4, achieving up to 2$\times$ speedup by skipping zero multiplications [@NVIDIA2020]. However, this acceleration requires the sparsity pattern to match hardware expectations, and unstructured sparsity typically sees limited benefit [@hoefler2021sparsity]. TPUs\index{TPU!sparse matrix support} provide support for sparse weight matrices, though this capability has evolved across generations. The original TPU was designed primarily for dense operations; later versions and research adaptations (such as Sparse-TPU) have added sparse matrix support. The systolic array architecture can process non-zero elements efficiently when sparsity patterns are predictable, making this particularly beneficial for transformer models where large matrix multiplications dominate [@jouppi2021ten]. FPGAs\index{FPGA!custom sparsity patterns} offer the most flexibility: unlike GPUs and TPUs, they can be programmed to handle arbitrary sparse formats, making them suitable for unstructured pruning or application-specific patterns where general-purpose accelerators underperform. +GPUs\index{Tensor Cores!sparse acceleration}\index{Hardware-Aware Optimization!sparse tensor cores} with Sparse Tensor Cores (NVIDIA Ampere and later) accelerate structured sparsity patterns like 2:4, achieving up to 2 $\times$ speedup by skipping zero multiplications [@NVIDIA2020]. However, this acceleration requires the sparsity pattern to match hardware expectations, and unstructured sparsity typically sees limited benefit [@hoefler2021sparsity]. TPUs\index{TPU!sparse matrix support} provide support for sparse weight matrices, though this capability has evolved across generations. The original TPU was designed primarily for dense operations; later versions and research adaptations (such as Sparse-TPU) have added sparse matrix support. The systolic array architecture can process non-zero elements efficiently when sparsity patterns are predictable, making this particularly beneficial for transformer models where large matrix multiplications dominate [@jouppi2021ten]. FPGAs\index{FPGA!custom sparsity patterns} offer the most flexibility: unlike GPUs and TPUs, they can be programmed to handle arbitrary sparse formats, making them suitable for unstructured pruning or application-specific patterns where general-purpose accelerators underperform. Across all platforms, sparse operations reduce memory bandwidth requirements and energy consumption by accessing fewer elements[^fn-sparse-energy-savings]. This benefit compounds with quantization: a sparse INT8 model requires less memory traffic than either technique alone [@Gale2020]. @@ -6387,8 +6387,8 @@ The following decision framework guides technique selection based on primary dep #### Model Size (Storage/Download) {.unnumbered} -- **First choice**: INT8 quantization via PTQ (4$\times$ reduction, minimal accuracy loss) -- **If more reduction needed**: INT4 quantization (8$\times$ reduction, 1–3% accuracy loss typical) +- **First choice**: INT8 quantization via PTQ (4 $\times$ reduction, minimal accuracy loss) +- **If more reduction needed**: INT4 quantization (8 $\times$ reduction, 1–3% accuracy loss typical) - **If accuracy-critical**: Knowledge distillation to smaller architecture + quantization #### Inference Latency {.unnumbered} @@ -6630,7 +6630,7 @@ Sequencing critically impacts results, as the following example demonstrates. ::: {.callout-example title="BERT-Base Mobile Deployment Pipeline"} -Consider deploying BERT-Base on mobile devices through three stages. **Stage one** applies structured pruning, removing 30% of attention heads and 40% of intermediate FFN dimensions, resulting in 75% parameter reduction with accuracy dropping from 76.2% to 75.1%. **Stage two** uses knowledge distillation to recover accuracy to 75.9%. **Stage three** applies quantization-aware training with INT8 quantization, achieving 4$\times$ additional memory reduction with final accuracy of 75.6%. The combined impact: 16$\times$ memory reduction (440MB to 28MB), 12$\times$ inference speedup on mobile CPU, and only 0.6% final accuracy loss versus 2.1% if quantization had been applied before pruning. +Consider deploying BERT-Base on mobile devices through three stages. **Stage one** applies structured pruning, removing 30% of attention heads and 40% of intermediate FFN dimensions, resulting in 75% parameter reduction with accuracy dropping from 76.2% to 75.1%. **Stage two** uses knowledge distillation to recover accuracy to 75.9%. **Stage three** applies quantization-aware training with INT8 quantization, achieving 4 $\times$ additional memory reduction with final accuracy of 75.6%. The combined impact: 16 $\times$ memory reduction (440MB to 28MB), 12 $\times$ inference speedup on mobile CPU, and only 0.6% final accuracy loss versus 2.1% if quantization had been applied before pruning. ::: This example illustrates why sequencing matters: pruning first concentrates important weights into smaller ranges, making subsequent quantization more effective. Applying quantization before pruning reduces numerical precision available for importance-based pruning decisions, degrading final accuracy. Effective combination requires understanding these dependencies and developing application sequences that maximize cumulative benefits. @@ -6655,7 +6655,7 @@ A critical caveat applies when translating profiling metrics into optimization e ::: {.callout-warning title="FLOPs Reduction ≠ Proportional Speedup"} -Reducing a model's FLOPs by 50% does not guarantee 50% latency reduction. Memory-bound operations (common in LLM inference and normalization layers) see minimal benefit from compute reduction because they are bottlenecked by data movement, not arithmetic. Critically, **Amdahl's Law**\index{Amdahl's Law!model compression} (@sec-machine-foundations-amdahls-law-gustafsons-law-b741) applies at the system level: if model inference accounts for only 20% of end-to-end latency (with the rest spent on data loading, pre-processing, and post-processing), then even perfect model optimization yields at most 1.25$\times$ overall speedup. Always profile on target hardware before estimating optimization benefits. +Reducing a model's FLOPs by 50% does not guarantee 50% latency reduction. Memory-bound operations (common in LLM inference and normalization layers) see minimal benefit from compute reduction because they are bottlenecked by data movement, not arithmetic. Critically, **Amdahl's Law**\index{Amdahl's Law!model compression} (@sec-machine-foundations-amdahls-law-gustafsons-law-b741) applies at the system level: if model inference accounts for only 20% of end-to-end latency (with the rest spent on data loading, pre-processing, and post-processing), then even perfect model optimization yields at most 1.25 $\times$ overall speedup. Always profile on target hardware before estimating optimization benefits. ::: Consider profiling a Vision Transformer (ViT) for edge deployment. Using PyTorch Profiler reveals three key findings: attention layers consume 65% of total FLOPs (highly amenable to structured pruning), layer normalization consumes 8% of latency despite only 2% of FLOPs (a memory-bound operation), and the final classification head consumes 1% of computation but 15% of parameter memory. This profile suggests a clear priority ordering: first, apply magnitude-based pruning to attention layers for high FLOP reduction; second, quantize the classification head to INT8 for large memory savings with minimal accuracy impact; third, fuse layer normalization operations to reduce the memory bandwidth bottleneck. @@ -6668,7 +6668,7 @@ Optimization requires rigorous measurement frameworks that go beyond simple accu The measurement framework should establish clear baselines before applying any optimizations. Accuracy baselines include not only top-line metrics like classification accuracy but also calibration, fairness across demographic groups, and robustness to input variations. Efficiency baselines capture computational cost (FLOPs, memory bandwidth), execution time across hardware platforms, peak memory consumption, and energy consumption profiles. -When quantizing ResNet-50 from FP32 to INT8, baseline metrics show Top-1 accuracy of 76.1%, inference latency on V100 of 4.2 ms, model size of 98 MB, and energy per inference of 0.31 J. Post-quantization metrics reveal Top-1 accuracy of 75.8% (0.3% degradation), inference latency of 1.3 ms (3.2$\times$ speedup), model size of 25 MB (3.9$\times$ reduction), and energy per inference of 0.08 J (3.9$\times$ improvement). Additional analysis shows per-class accuracy degradation ranging from 0.1% to 1.2% with highest impact on fine-grained categories, calibration error increasing from 2.1% to 3.4%, and INT8 quantization providing 3.2$\times$ speedup on GPU but only 1.8$\times$ on CPU, demonstrating hardware-dependent gains. +When quantizing ResNet-50 from FP32 to INT8, baseline metrics show Top-1 accuracy of 76.1%, inference latency on V100 of 4.2 ms, model size of 98 MB, and energy per inference of 0.31 J. Post-quantization metrics reveal Top-1 accuracy of 75.8% (0.3% degradation), inference latency of 1.3 ms (3.2 $\times$ speedup), model size of 25 MB (3.9 $\times$ reduction), and energy per inference of 0.08 J (3.9 $\times$ improvement). Additional analysis shows per-class accuracy degradation ranging from 0.1% to 1.2% with highest impact on fine-grained categories, calibration error increasing from 2.1% to 3.4%, and INT8 quantization providing 3.2 $\times$ speedup on GPU but only 1.8 $\times$ on CPU, demonstrating hardware-dependent gains. With these comprehensive baselines in place, the measurement framework must track optimization impact systematically. Rather than evaluating techniques in isolation, applying our three-dimensional framework requires understanding how different approaches interact when combined. Sequential application can lead to compounding benefits or unexpected interactions that diminish overall effectiveness. @sec-benchmarking provides additional structured evaluation methods for comprehensive performance assessment. @@ -6775,7 +6775,7 @@ A comparative analysis across the three major approaches reveals how each addres : **Optimization Technique Trade-offs**: Comparison of the three major optimization approaches across key performance dimensions, highlighting how each technique addresses different constraints and deployment scenarios. Pruning excels for computational reduction but requires sparse hardware support, quantization provides balanced size and speed improvements with wide hardware compatibility, while distillation produces high-quality compressed models at higher training cost. {#tbl-optimization-comparison} \index{Model Compression!sequential application} -These techniques combine synergistically, with quantization often applied after pruning or distillation to achieve compound compression benefits. Production systems frequently employ sequential application: initial pruning reduces parameter count, quantization optimizes numerical representation, and fine-tuning through distillation principles recovers any accuracy loss. Sequential application enables compression ratios of 10–50$\times$ while maintaining competitive accuracy across diverse deployment scenarios. +These techniques combine synergistically, with quantization often applied after pruning or distillation to achieve compound compression benefits. Production systems frequently employ sequential application: initial pruning reduces parameter count, quantization optimizes numerical representation, and fine-tuning through distillation principles recovers any accuracy loss. Sequential application enables compression ratios of 10–50 $\times$ while maintaining competitive accuracy across diverse deployment scenarios. With the complete optimization toolkit now surveyed—from individual techniques through combination strategies—the most instructive lessons often come not from what works but from what fails. The following fallacies and pitfalls capture the most common mistakes engineers make when applying these techniques, each grounded in the quantitative trade-offs we have established throughout the chapter. @@ -6836,11 +6836,11 @@ Model optimization involves counterintuitive interactions between techniques tha **Fallacy:** *Optimization techniques can be applied independently without considering their interactions.* -Engineers assume optimization strategies compose additively: 50% pruning plus `{python} int8_size_reduction_str`$\times$ quantization yields combined benefits. In reality, techniques interact non-linearly and compound losses. A BERT model pruned to `{python} pruning_target_str`% parameters maintains 97.8% performance, but applying INT8 quantization afterward drops accuracy to 94.2%, while QAT on the pruned model achieves 96.5%. Knowledge distillation from heavily pruned teachers transfers degenerate attention patterns that reduce student accuracy by 3–5% compared to distilling from dense models. As @sec-model-compression-optimization-strategies-f2f6 demonstrates, successful optimization requires coordinated application where techniques are sequenced together. Organizations that apply aggressive combinations without measuring interactions waste weeks recovering lost accuracy. +Engineers assume optimization strategies compose additively: 50% pruning plus `{python} int8_size_reduction_str` $\times$ quantization yields combined benefits. In reality, techniques interact non-linearly and compound losses. A BERT model pruned to `{python} pruning_target_str`% parameters maintains 97.8% performance, but applying INT8 quantization afterward drops accuracy to 94.2%, while QAT on the pruned model achieves 96.5%. Knowledge distillation from heavily pruned teachers transfers degenerate attention patterns that reduce student accuracy by 3–5% compared to distilling from dense models. As @sec-model-compression-optimization-strategies-f2f6 demonstrates, successful optimization requires coordinated application where techniques are sequenced together. Organizations that apply aggressive combinations without measuring interactions waste weeks recovering lost accuracy. **Pitfall:** *Optimizing for theoretical metrics rather than actual deployment performance.* -Teams reduce FLOPs by 60% and celebrate efficiency gains without profiling deployment hardware. A pruned model with `{python} param_removal_str`% fewer parameters shows irregular sparsity patterns that prevent vectorization, achieving only 12% latency reduction instead of the expected `{python} param_removal_str`% on ARM processors. INT8 quantization reduces a transformer from `{python} bert_fp32_mb_str` MB to `{python} bert_int8_mb_str` MB, but dequantization overhead on GPUs lacking low-precision acceleration increases latency by 15% despite the `{python} int8_size_reduction_str`$\times$ size reduction. As shown in @sec-model-compression-profiling-opportunity-analysis-477f, memory bandwidth, cache behavior, and instruction-level parallelism determine actual performance, not operation counts. Production deployments require measuring wall-clock latency on target hardware. +Teams reduce FLOPs by 60% and celebrate efficiency gains without profiling deployment hardware. A pruned model with `{python} param_removal_str`% fewer parameters shows irregular sparsity patterns that prevent vectorization, achieving only 12% latency reduction instead of the expected `{python} param_removal_str`% on ARM processors. INT8 quantization reduces a transformer from `{python} bert_fp32_mb_str` MB to `{python} bert_int8_mb_str` MB, but dequantization overhead on GPUs lacking low-precision acceleration increases latency by 15% despite the `{python} int8_size_reduction_str` $\times$ size reduction. As shown in @sec-model-compression-profiling-opportunity-analysis-477f, memory bandwidth, cache behavior, and instruction-level parallelism determine actual performance, not operation counts. Production deployments require measuring wall-clock latency on target hardware. **Fallacy:** *Aggressive quantization maintains model performance with minimal accuracy loss.* @@ -6852,22 +6852,22 @@ Teams apply post-training quantization (PTQ) to avoid retraining and achieve 96. **Pitfall:** *Assuming compression ratios translate directly into proportional deployment gains.* -Teams achieve `{python} int8_size_reduction_str`$\times$ model size reduction through INT8 quantization and expect `{python} int8_size_reduction_str`$\times$ memory savings in deployment. In practice, runtime overhead erodes compression gains. Dequantization kernels add `{python} dequant_overhead_str`% latency overhead converting INT8 weights back to FP16. Pruned models with irregular sparsity achieve only 12% latency reduction despite `{python} param_removal_str`% parameter removal because hardware cannot skip zeroed weights efficiently. As @sec-model-compression-profiling-opportunity-analysis-477f demonstrates, a BERT model pruned to 50% sparsity and quantized to INT8 achieves `{python} actual_speedup_str`% end-to-end speedup rather than the expected `{python} expected_speedup_str`$\times$, because unstructured sparsity creates irregular memory access. Production workflows must profile *deployed* latency on target hardware, not extrapolate from compression ratios. +Teams achieve `{python} int8_size_reduction_str` $\times$ model size reduction through INT8 quantization and expect `{python} int8_size_reduction_str` $\times$ memory savings in deployment. In practice, runtime overhead erodes compression gains. Dequantization kernels add `{python} dequant_overhead_str`% latency overhead converting INT8 weights back to FP16. Pruned models with irregular sparsity achieve only 12% latency reduction despite `{python} param_removal_str`% parameter removal because hardware cannot skip zeroed weights efficiently. As @sec-model-compression-profiling-opportunity-analysis-477f demonstrates, a BERT model pruned to 50% sparsity and quantized to INT8 achieves `{python} actual_speedup_str`% end-to-end speedup rather than the expected `{python} expected_speedup_str` $\times$, because unstructured sparsity creates irregular memory access. Production workflows must profile *deployed* latency on target hardware, not extrapolate from compression ratios. ## Summary {#sec-model-compression-summary-8229} -Model compression is not a bag of tricks but an engineering discipline built on three complementary dimensions: *structural optimization* determines what the model computes, *precision optimization* determines how precisely it computes, and *architectural optimization* determines how efficiently those computations execute on physical hardware. The most important lesson of this chapter is that these dimensions compose multiplicatively. Pruning alone might achieve 2$\times$ compression; quantization alone might achieve 4$\times$; but pruning, distillation, and quantization applied together can achieve 16$\times$ — as BERT's compression from 440MB to 28MB demonstrates. The second lesson is equally important: theoretical compression ratios lie. A 4$\times$ reduction in parameters translates to 4$\times$ latency improvement only when the optimization aligns with the hardware's execution model. Unstructured sparsity on hardware that lacks sparse kernels achieves almost nothing; INT8 quantization on hardware without INT8 units achieves even less. Profile on target hardware, not paper metrics. +Model compression is not a bag of tricks but an engineering discipline built on three complementary dimensions: *structural optimization* determines what the model computes, *precision optimization* determines how precisely it computes, and *architectural optimization* determines how efficiently those computations execute on physical hardware. The most important lesson of this chapter is that these dimensions compose multiplicatively. Pruning alone might achieve 2 $\times$ compression; quantization alone might achieve 4 $\times$; but pruning, distillation, and quantization applied together can achieve 16 $\times$ — as BERT's compression from 440MB to 28MB demonstrates. The second lesson is equally important: theoretical compression ratios lie. A 4 $\times$ reduction in parameters translates to 4 $\times$ latency improvement only when the optimization aligns with the hardware's execution model. Unstructured sparsity on hardware that lacks sparse kernels achieves almost nothing; INT8 quantization on hardware without INT8 units achieves even less. Profile on target hardware, not paper metrics. Combined with the data selection techniques from @sec-data-selection, these model-centric optimizations complete the efficiency toolkit: data selection maximizes learning from available examples, while model compression minimizes resources required for deployment. ::: {.callout-takeaways title="From Benchmark Winner to Production Model"} * **Three dimensions of optimization**: Structural (what to compute), precision (how precisely), architectural (how efficiently). Combine all three for maximum compression. -* **PTQ is a strong baseline**: INT8 post-training quantization requires no retraining and delivers 4$\times$ compression. Use QAT or distillation when baseline accuracy is insufficient for the application. +* **PTQ is a strong baseline**: INT8 post-training quantization requires no retraining and delivers 4 $\times$ compression. Use QAT or distillation when baseline accuracy is insufficient for the application. * **For LLMs, weight-only quantization wins**: INT4 weights with FP16 activations dominates because generation is memory-bandwidth bound, not compute bound. * **Structured pruning for commodity hardware**: Unstructured sparsity requires specialized accelerators. Structured pruning (channels, heads) delivers real latency gains on GPUs. * **Order matters when combining techniques**: Pruning before quantization is more effective; architecture changes should align with quantization constraints. Distillation can mitigate quantization accuracy loss. -* **Profile on target hardware, not paper metrics**: FLOPs and parameter count often mispredict real-world performance. A 2$\times$ FLOP reduction may yield only 1.2$\times$ speedup. +* **Profile on target hardware, not paper metrics**: FLOPs and parameter count often mispredict real-world performance. A 2 $\times$ FLOP reduction may yield only 1.2 $\times$ speedup. * **Hardware-aware design turns compression into real speedups**: Align pruning structure, quantization format, and operator choices with the capabilities of the target accelerator to convert theoretical savings into measured latency gains. ::: diff --git a/book/quarto/contents/vol1/parts/optimize_principles.qmd b/book/quarto/contents/vol1/parts/optimize_principles.qmd index 41578ef3a0..47e2ae65bf 100644 --- a/book/quarto/contents/vol1/parts/optimize_principles.qmd +++ b/book/quarto/contents/vol1/parts/optimize_principles.qmd @@ -34,7 +34,7 @@ $$ R = \min(R_{peak}, I \times BW) $$ The majority of ML workloads fall on the memory-bound side of the ridge point. This is not coincidence but a consequence of the **Memory Wall**: processor speed has historically outpaced memory bandwidth, and the cumulative gap has widened over three decades. Neural networks, with their massive weight tensors and poor temporal locality, are especially vulnerable. The Arithmetic Intensity Law diagnoses *where* you sit relative to this wall; the next principle explains *why* that wall is so punishing: ::: {.callout-principle title="The Energy-Movement Invariant" icon="false"} -**The Invariant**: Moving 1 bit of data from DRAM costs 100–1,000$\times$ more energy than performing an arithmetic operation on it. +**The Invariant**: Moving 1 bit of data from DRAM costs 100–1,000 $\times$ more energy than performing an arithmetic operation on it. $$ E_{move} \gg E_{compute} $$ **The Implication**: **Data Locality** is the primary driver of efficiency. Optimization strategies must prioritize **kernel fusion** (keeping data in registers) and **quantization** (reducing data size) over reducing raw operation counts. @@ -47,7 +47,7 @@ Even with perfect data locality and optimal bottleneck targeting, a final constr $$ \text{Speedup} = \frac{1}{(1-p) + \frac{p}{s}} $$ where $p$ is the parallelizable fraction and $s$ is the speedup of that fraction. -**The Implication**: If 95% of your model runs 100$\times$ faster on a GPU, your total system speedup is capped at ~16.8$\times$. This explains *why* **data loading** and **preprocessing** often become the ultimate bottlenecks in highly optimized systems. +**The Implication**: If 95% of your model runs 100 $\times$ faster on a GPU, your total system speedup is capped at ~16.8 $\times$. This explains *why* **data loading** and **preprocessing** often become the ultimate bottlenecks in highly optimized systems. ::: Part III applies these principles systematically through the D·A·M taxonomy — Data, Algorithm, Machine — asking first whether the work is necessary, then whether it can be simplified, and finally how to do it faster (see @sec-dam-taxonomy for the full diagnostic framework). diff --git a/book/quarto/contents/vol1/responsible_engr/responsible_engr.qmd b/book/quarto/contents/vol1/responsible_engr/responsible_engr.qmd index 04ae3230fb..cdbbcfc055 100644 --- a/book/quarto/contents/vol1/responsible_engr/responsible_engr.qmd +++ b/book/quarto/contents/vol1/responsible_engr/responsible_engr.qmd @@ -19,7 +19,7 @@ glossary: responsible_engr_glossary.json ## Purpose {.unnumbered} \begin{marginfigure} -\mlsysstack{0}{0}{10}{10}{20}{40}{90}{25} +\mlsysstack{0}{0}{10}{10}{20}{40}{90}{15} \end{marginfigure} _Why is a system that does exactly what it was told to do often the most dangerous?_ @@ -192,7 +192,7 @@ The preceding examples emphasize failure, but responsible engineering also produ Apple's deployment of **differential privacy**\index{Differential Privacy!mathematical guarantees} in iOS represents responsible engineering at scale.[^fn-differential-privacy] The system collects usage data for product improvement while providing mathematical guarantees about individual privacy. The implementation required substantial engineering investment: noise calibration to balance utility against privacy, distributed computation to minimize data exposure, and transparent documentation of privacy parameters. The business value: Apple differentiated on privacy as a product feature, enabling data collection that would otherwise face regulatory and reputational barriers. -[^fn-differential-privacy]: **Differential Privacy**: A mathematical framework introduced by Dwork et al. [@dwork2006calibrating] providing formal privacy guarantees. A mechanism satisfies $\epsilon$-differential privacy if the probability of any output changes by at most a factor of $e^\epsilon$ when any single individual's data is added or removed. Smaller $\epsilon$ values provide stronger privacy but reduce data utility. Key systems trade-offs: 15--30% computational overhead, 10–100$\times$ more data needed for equivalent accuracy, and careful privacy budget management across queries. +[^fn-differential-privacy]: **Differential Privacy**: A mathematical framework introduced by Dwork et al. [@dwork2006calibrating] providing formal privacy guarantees. A mechanism satisfies $\epsilon$-differential privacy if the probability of any output changes by at most a factor of $e^\epsilon$ when any single individual's data is added or removed. Smaller $\epsilon$ values provide stronger privacy but reduce data utility. Key systems trade-offs: 15--30% computational overhead, 10–100 $\times$ more data needed for equivalent accuracy, and careful privacy budget management across queries. \index{Algorithmic Transparency!user controls}Spotify addressed recommendation system concerns by implementing transparency features showing users why songs were recommended and providing controls to adjust algorithm behavior. This engineering investment served multiple purposes: user trust through explainability, reduced filter bubble effects through diversity injection, and regulatory compliance through user control mechanisms. The approach demonstrates that responsibility features can enhance rather than constrain product value. @@ -308,20 +308,20 @@ acc_light_str = GenderShadesDisparity.acc_light_str acc_dark_str = GenderShadesDisparity.acc_dark_str ``` -| **Demographic Group** | **Error Rate (%)** | **Relative Disparity** | -|:--------------------------|----------------------------------:|-----------------------------------------------------:| -| **Light-skinned males** | `{python} error_light_male_str` | Baseline (1.0$\times$) | -| **Light-skinned females** | `{python} error_light_female_str` | `{python} disparity_light_female_str`$\times$ higher | -| **Dark-skinned males** | `{python} error_dark_male_str` | `{python} disparity_dark_male_str`$\times$ higher | -| **Dark-skinned females** | `{python} error_dark_female_str` | `{python} disparity_str`$\times$ higher | +| **Demographic Group** | **Error Rate (%)** | **Relative Disparity** | +|:--------------------------|----------------------------------:|------------------------------------------------------:| +| **Light-skinned males** | `{python} error_light_male_str` | Baseline (1.0 $\times$) | +| **Light-skinned females** | `{python} error_light_female_str` | `{python} disparity_light_female_str` $\times$ higher | +| **Dark-skinned males** | `{python} error_dark_male_str` | `{python} disparity_dark_male_str` $\times$ higher | +| **Dark-skinned females** | `{python} error_dark_female_str` | `{python} disparity_str` $\times$ higher | -: **Gender Shades Facial Recognition Error Rates**: Disaggregated evaluation reveals that aggregate accuracy metrics conceal severe performance disparities. Systems that appear highly accurate overall show error rates varying by more than 43$\times$ across demographic groups. Worst-case results across systems studied; source: @buolamwini2018gender. {#tbl-gender-shades-results} +: **Gender Shades Facial Recognition Error Rates**: Disaggregated evaluation reveals that aggregate accuracy metrics conceal severe performance disparities. Systems that appear highly accurate overall show error rates varying by more than 43 $\times$ across demographic groups. Worst-case results across systems studied; source: @buolamwini2018gender. {#tbl-gender-shades-results} [^fn-gender-shades]: **Gender Shades**: A landmark 2018 study by Joy Buolamwini and Timnit Gebru at MIT Media Lab that audited commercial facial recognition systems from Microsoft, IBM, and Face++. The name evokes both the demographic dimensions studied (gender, skin shade) and the "shades of gray" in algorithmic accountability. Using the Fitzpatrick skin type scale from dermatology, they created a balanced benchmark (Pilot Parliaments Benchmark) with equal representation across gender and skin tone. The study's methodology became a template for algorithmic auditing, and its findings directly prompted Microsoft and IBM to improve their systems. As @tbl-gender-shades-results quantifies, disaggregated evaluation revealed what aggregate accuracy scores concealed. Systems reporting high overall accuracy simultaneously achieved error rates as low as `{python} error_light_male_str`% for light-skinned males and as high as `{python} error_dark_female_str`% for dark-skinned females (corresponding to accuracies of `{python} acc_light_str`% and `{python} acc_dark_str`% respectively). The aggregate metric provided no indication of this `{python} disparity_str`-fold disparity in error rates. -While no universal threshold defines acceptable disparity, engineering teams should establish explicit bounds before deployment. Common industry practices include error rate ratios below 1.25$\times$ between demographic groups for high-stakes applications, false positive rate differences under 5 percentage points for screening systems, and selection rate ratios of at least 0.8 relative to the highest group's rate (the four-fifths rule\index{Four-Fifths Rule!disparate impact threshold} from employment discrimination law).\index{Disparate Impact!statistical threshold}[^fn-disparate-impact][^fn-four-fifths-rule] These thresholds are starting points for discussion with stakeholders, not absolute standards. The key engineering discipline is defining measurable criteria before deployment rather than discovering problems after harm has occurred. +While no universal threshold defines acceptable disparity, engineering teams should establish explicit bounds before deployment. Common industry practices include error rate ratios below 1.25 $\times$ between demographic groups for high-stakes applications, false positive rate differences under 5 percentage points for screening systems, and selection rate ratios of at least 0.8 relative to the highest group's rate (the four-fifths rule\index{Four-Fifths Rule!disparate impact threshold} from employment discrimination law).\index{Disparate Impact!statistical threshold}[^fn-disparate-impact][^fn-four-fifths-rule] These thresholds are starting points for discussion with stakeholders, not absolute standards. The key engineering discipline is defining measurable criteria before deployment rather than discovering problems after harm has occurred. [^fn-disparate-impact]: **Disparate Impact**\index{Disparate Impact!etymology}: A legal concept from *Griggs v. Duke Power Co.* (1971), where the US Supreme Court held that practices "fair in form, but discriminatory in operation" violate civil rights law even without intent. The term distinguishes *disparate impact* (unintentional statistical harm) from *disparate treatment* (intentional discrimination). For ML, this is critical: models trained on historical data can produce disparate impact even when protected attributes are excluded, because proxy variables correlate with protected characteristics. @@ -366,7 +366,7 @@ A second mechanism is regulatory compliance\index{Regulatory Compliance!business Competitive differentiation\index{Trust!competitive differentiation} completes the business case. Trust increasingly drives enterprise purchasing decisions for ML-powered services, and organizations that can demonstrate systematic responsibility practices through model cards, audit trails, and published evaluation results win contracts that competitors cannot. Apple's privacy positioning, Microsoft's responsible AI principles, and Anthropic's safety research all represent strategic investments in responsibility as differentiation. -The quantization techniques from @sec-model-compression reduce inference energy by 2–4$\times$, directly supporting sustainable deployment. The monitoring infrastructure from @sec-ml-operations enables disaggregated fairness evaluation across demographic groups. Responsible engineering synthesizes these capabilities into disciplined practice through structured frameworks that translate principles into processes. +The quantization techniques from @sec-model-compression reduce inference energy by 2–4 $\times$, directly supporting sustainable deployment. The monitoring infrastructure from @sec-ml-operations enables disaggregated fairness evaluation across demographic groups. Responsible engineering synthesizes these capabilities into disciplined practice through structured frameworks that translate principles into processes. The preceding sections established *why* ML systems fail and *who* must lead on responsibility. Knowing that engineers must lead is insufficient without knowing *how*. The cases above reveal a pattern: every failure could have been prevented by systematic processes applied at the right stage of development. What was missing was not technical capability but disciplined practice: checklists, documentation standards, testing protocols, and monitoring infrastructure that translate responsibility principles into repeatable engineering workflows. @@ -453,7 +453,7 @@ Ntotal = `{python} repr_target_images_str` / `{python} repr_group_fraction_str` **Stratified Sampling**: If you specifically target this group (e.g., via active learning or community outreach), you only need: `{python} repr_equation_md` -**The Insight**: Relying on "natural distribution" data for fairness is physically impossible at scale. You effectively need `{python} repr_multiplier_str`$\times$ more data to validate the minority group than the majority group. Fairness requires **intentional data engineering**, not just more data. +**The Insight**: Relying on "natural distribution" data for fairness is physically impossible at scale. You effectively need `{python} repr_multiplier_str` $\times$ more data to validate the minority group than the majority group. Fairness requires **intentional data engineering**, not just more data. ::: \index{Human-in-the-Loop (HITL)!decision oversight}For high-stakes applications, the deployment phase should specify where human oversight is required.\index{Safety!human oversight requirements} Human-in-the-loop (HITL) systems route uncertain, high-consequence, or flagged decisions to human reviewers rather than acting autonomously. The design questions are: Which decisions require human review? What confidence thresholds trigger escalation? How are reviewers trained and monitored? HITL is not a catch-all solution: human reviewers can rubber-stamp automated decisions, introduce their own biases, or become overwhelmed by alert volume. Effective HITL design requires calibrating the human-machine boundary to the specific application risks and reviewer capabilities. @@ -488,7 +488,7 @@ Datasheets for datasets\index{Datasheets for Datasets!training data documentatio ### Testing Across Populations {#sec-responsible-engineering-testing-across-populations-9f20} -Aggregate performance metrics\index{Aggregate Metrics!flaw of averages} mask significant disparities across user populations, illustrating the **Flaw of Averages** [@savage2009flaw]. As shown in @tbl-gender-shades-results, systems can appear highly accurate in aggregate while showing more than 40$\times$ error rate disparities across demographic groups. Responsible testing requires disaggregated evaluation that examines performance for relevant subgroups. +Aggregate performance metrics\index{Aggregate Metrics!flaw of averages} mask significant disparities across user populations, illustrating the **Flaw of Averages** [@savage2009flaw]. As shown in @tbl-gender-shades-results, systems can appear highly accurate in aggregate while showing more than 40 $\times$ error rate disparities across demographic groups. Responsible testing requires disaggregated evaluation that examines performance for relevant subgroups. ::: {.callout-perspective title="The Flaw of Averages"} **Averages Hide Failures**: In systems engineering, we rarely design for the "average" case; we design for the **tail cases** and **boundary conditions**. A bridge that is "safe on average" but collapses under a heavy truck is a failure. Similarly, an ML system that is "accurate on average" but fails for a specific ethnic or gender group is an engineering failure. The same principle that drives us to measure **tail latency** (p99) for system reliability applies to fairness: we must use **disaggregated evaluation** to measure system fairness. If you only look at the aggregate accuracy, you are blinded to the systemic failures occurring in the margins. Responsible engineering requires making these "tails" visible through granular, population-specific measurement. @@ -637,7 +637,7 @@ b_total_str = LoanFairness.b_total_str Three standard fairness metrics\index{Fairness Metrics!confusion matrix analysis} computed from the confusion matrices[^fn-confusion-matrix] in @tbl-confusion-group-a and @tbl-confusion-group-b\index{Confusion Matrix!fairness computation} reveal significant disparities.[^fn-fairness-metrics-origins] -[^fn-confusion-matrix]: **Confusion Matrix**: The term "confusion" refers to the matrix's ability to reveal *where* a classifier gets confused—that is, which classes it misidentifies as which. The concept dates to Karl Pearson's contingency tables (1904), but the specific term "confusion matrix" was introduced by S. Joshi in 1975. The 2$\times$2 binary classification version contains four cells: True Positives (TP), False Positives (FP, also called Type I errors), True Negatives (TN), and False Negatives (FN, Type II errors). For responsible engineering, the confusion matrix is foundational because *every* fairness metric—demographic parity, equal opportunity, equalized odds, calibration—is computed from these four cells. Computing separate confusion matrices per demographic group, as this chapter demonstrates, transforms an aggregate view into a disaggregated analysis that reveals disparities invisible in overall metrics. +[^fn-confusion-matrix]: **Confusion Matrix**: The term "confusion" refers to the matrix's ability to reveal *where* a classifier gets confused—that is, which classes it misidentifies as which. The concept dates to Karl Pearson's contingency tables (1904), but the specific term "confusion matrix" was introduced by S. Joshi in 1975. The 2 $\times$ 2 binary classification version contains four cells: True Positives (TP), False Positives (FP, also called Type I errors), True Negatives (TN), and False Negatives (FN, Type II errors). For responsible engineering, the confusion matrix is foundational because *every* fairness metric—demographic parity, equal opportunity, equalized odds, calibration—is computed from these four cells. Computing separate confusion matrices per demographic group, as this chapter demonstrates, transforms an aggregate view into a disaggregated analysis that reveals disparities invisible in overall metrics. [^fn-fairness-metrics-origins]: **Fairness Metrics Origins**: These metrics formalize concepts from civil rights law into mathematical constraints. "Demographic parity" (also called "statistical parity") requires outcomes independent of group membership, echoing the principle behind the 1964 Civil Rights Act. "Equal opportunity" and "equalized odds" were formalized by Hardt, Price, and Srebro [@hardt2016equality], who demonstrated that different fairness definitions are mathematically incompatible. This impossibility result, proven by Chouldechova [@chouldechova2017fair], shows that except in special cases, no classifier can simultaneously satisfy calibration and equal error rates across groups. @@ -791,7 +791,7 @@ The level of explainability required varies by application context and regulator Engineering teams should select explainability approaches based on these domain requirements. Post-hoc explanation methods\index{Explainability!post-hoc methods (SHAP, LIME)}\index{SHAP!feature attribution}\index{LIME!local explanations} (LIME, SHAP) generate feature importance scores\index{Feature Importance!prediction attribution} for individual predictions without requiring model architecture changes.[^fn-lime-shap] Inherently interpretable models\index{Interpretable Models!transparency by design} (linear models, decision trees, attention mechanisms) provide explanations as part of their structure but may sacrifice predictive performance. Concept-based explanations\index{Concept-based Explanations!human-understandable features} map model behavior to human-understandable concepts rather than raw features. The choice involves trade-offs between explanation fidelity, computational cost, and model flexibility. To see how these trade-offs align with model architecture, trace the spectrum of interpretability in @fig-interpretability-spectrum from left to right. Notice that the spectrum does not imply "simple is always better." A highly interpretable model that makes wrong predictions serves no one. The engineering challenge is selecting the most interpretable model that meets accuracy requirements for the application. -[^fn-lime-shap]: **LIME and SHAP**: Two dominant post-hoc explainability methods with different computational trade-offs. LIME (Local Interpretable Model-agnostic Explanations) [@ribeiro2016why] fits a simple interpretable model around each prediction, offering faster computation but potentially inconsistent explanations. SHAP derives its name from SHapley Additive exPlanations, honoring Lloyd Shapley, the mathematician who introduced Shapley values in his 1953 game theory work on fair allocation of cooperative gains. Lundberg and Lee [@lundberg2017unified] adapted this framework to compute feature contributions, providing mathematically consistent explanations but with exponential worst-case complexity. Shapley received the 2012 Nobel Prize in Economics for this foundational work. Systems implication: SHAP may add 10–100$\times$ inference latency, making LIME preferable for real-time applications. +[^fn-lime-shap]: **LIME and SHAP**: Two dominant post-hoc explainability methods with different computational trade-offs. LIME (Local Interpretable Model-agnostic Explanations) [@ribeiro2016why] fits a simple interpretable model around each prediction, offering faster computation but potentially inconsistent explanations. SHAP derives its name from SHapley Additive exPlanations, honoring Lloyd Shapley, the mathematician who introduced Shapley values in his 1953 game theory work on fair allocation of cooperative gains. Lundberg and Lee [@lundberg2017unified] adapted this framework to compute feature contributions, providing mathematically consistent explanations but with exponential worst-case complexity. Shapley received the 2012 Nobel Prize in Economics for this foundational work. Systems implication: SHAP may add 10–100 $\times$ inference latency, making LIME preferable for real-time applications. ![**Model Interpretability Spectrum**. A horizontal spectrum arranges model architectures from most interpretable on the left (decision trees, linear regression, logistic regression) to least interpretable on the right (random forests, neural networks, convolutional neural networks). Models on the left allow direct inspection of decision logic, while those on the right require post-hoc explanation techniques such as LIME or SHAP. High-stakes regulatory requirements may constrain model selection toward the interpretable end of this spectrum.](images/svg/interpretability_spectrum.svg){#fig-interpretability-spectrum fig-alt="Horizontal spectrum showing model types from more interpretable (decision trees, linear regression, logistic regression) to less interpretable (random forest, neural network, convolutional neural network)."} @@ -866,11 +866,11 @@ In 2020, researchers estimated that training a single large NLP model emitted as The computational demands of modern ML systems have grown dramatically. Training large language models requires thousands of GPU hours, consuming energy measured in megawatt-hours.\index{Energy Consumption!training costs} Much of this expense, however, is not intrinsic to the learning task but represents *accidental complexity*: training from scratch when fine-tuning would suffice, using larger models than tasks require, and running hyperparameter searches that explore redundant configurations. Computational cost is largely a function of engineering discipline, not just model physics.\index{Green AI!efficiency as metric}[^fn-green-ai] -[^fn-green-ai]: **Green AI Movement**: Schwartz et al. [@schwartz2020green] contrast "Red AI" (performance at any cost) with "Green AI" (efficiency as primary metric). They propose reporting FLOPs alongside accuracy, documenting that state-of-the-art accuracy gains from 2012–2018 (AlexNet to AlphaZero) required a 300,000$\times$ compute increase, with NLP models following a similar exponential trend. Responsible engineering embraces Green AI: optimizing for performance-per-watt and carbon-aware training. +[^fn-green-ai]: **Green AI Movement**: Schwartz et al. [@schwartz2020green] contrast "Red AI" (performance at any cost) with "Green AI" (efficiency as primary metric). They propose reporting FLOPs alongside accuracy, documenting that state-of-the-art accuracy gains from 2012–2018 (AlexNet to AlphaZero) required a 300,000 $\times$ compute increase, with NLP models following a similar exponential trend. Responsible engineering embraces Green AI: optimizing for performance-per-watt and carbon-aware training. -Resource efficiency and responsible engineering are directly linked through three interconnected channels. The most direct connection is environmental\index{Carbon Footprint!computational emissions}: a model that requires 4$\times$ more compute than necessary generates 4$\times$ more carbon emissions, so the efficiency techniques from @sec-model-compression that enable edge deployment also reduce the environmental footprint of cloud inference. Efficiency also drives accessibility\index{Accessibility!resource-efficient models}—resource-efficient models can run on less expensive hardware, democratizing access to ML capabilities. A quantized model that runs on a smartphone enables users who cannot afford cloud API costs. Finally, sustainability at scale amplifies both effects: systems serving millions of users multiply inefficiencies across every request, so a 10 ms latency reduction per query translates to thousands of GPU-hours saved annually. +Resource efficiency and responsible engineering are directly linked through three interconnected channels. The most direct connection is environmental\index{Carbon Footprint!computational emissions}: a model that requires 4 $\times$ more compute than necessary generates 4 $\times$ more carbon emissions, so the efficiency techniques from @sec-model-compression that enable edge deployment also reduce the environmental footprint of cloud inference. Efficiency also drives accessibility\index{Accessibility!resource-efficient models}—resource-efficient models can run on less expensive hardware, democratizing access to ML capabilities. A quantized model that runs on a smartphone enables users who cannot afford cloud API costs. Finally, sustainability at scale amplifies both effects: systems serving millions of users multiply inefficiencies across every request, so a 10 ms latency reduction per query translates to thousands of GPU-hours saved annually. -The techniques from earlier chapters directly serve responsibility goals. Quantization\index{Quantization!environmental benefits} (@sec-model-compression) reduces compute by 2–4$\times$ with minimal accuracy impact. Pruning\index{Pruning!carbon reduction} removes 50–90% of parameters. Knowledge distillation\index{Knowledge Distillation!efficiency gains} typically achieves 5–20$\times$ compression while retaining 90–95% of the original accuracy. Hardware acceleration (@sec-hardware-acceleration) achieves 10–100$\times$ better energy efficiency than general-purpose processors. +The techniques from earlier chapters directly serve responsibility goals. Quantization\index{Quantization!environmental benefits} (@sec-model-compression) reduces compute by 2–4 $\times$ with minimal accuracy impact. Pruning\index{Pruning!carbon reduction} removes 50–90% of parameters. Knowledge distillation\index{Knowledge Distillation!efficiency gains} typically achieves 5–20 $\times$ compression while retaining 90–95% of the original accuracy. Hardware acceleration (@sec-hardware-acceleration) achieves 10–100 $\times$ better energy efficiency than general-purpose processors. Responsible engineers apply these techniques as design requirements, not afterthoughts. The question shifts from "What is the most accurate model?" to "What is the most accurate model that meets our efficiency constraints?" @@ -1278,7 +1278,7 @@ Operational costs encompass infrastructure, personnel, and incident response. @t | **Incident response (estimated)** | `{python} o_incident_annual_str` | `{python} o_incident_str` | | **Total operational** | | **`{python} o_total_str`** | -: **Operational Cost Calculation**: Operational costs include monitoring infrastructure ($50K/year), on-call engineering at 0.5 FTE ($100K/year), and incident response reserves ($20K/year). The $510K three-year total represents 25% of TCO and often surprises teams focused primarily on compute costs. These estimates represent minimum staffing; production systems at this scale typically require 2–5$\times$ more engineering support. These expenses persist regardless of model performance and grow with system complexity. {#tbl-tco-operations} +: **Operational Cost Calculation**: Operational costs include monitoring infrastructure ($50K/year), on-call engineering at 0.5 FTE ($100K/year), and incident response reserves ($20K/year). The $510K three-year total represents 25% of TCO and often surprises teams focused primarily on compute costs. These estimates represent minimum staffing; production systems at this scale typically require 2–5 $\times$ more engineering support. These expenses persist regardless of model performance and grow with system complexity. {#tbl-tco-operations} ```{python} #| label: tco-summary-calc @@ -1357,7 +1357,7 @@ Total cost of ownership reveals where responsible optimization has the most leve #### Environmental Impact {#sec-responsible-engineering-environmental-impact-0292} -\index{Environmental Impact!carbon accounting}The TCO analysis above captures costs that appear on invoices, but computational resources carry costs that no invoice reflects. Environmental impact follows from computational efficiency: the same optimization techniques that reduce TCO also reduce carbon emissions. The optimization techniques from @sec-hardware-acceleration and @sec-model-compression reduce energy consumption per inference, directly lowering carbon footprint. Data centers consume an estimated 1–2% of global electricity\index{Data Centers!energy consumption}, a share that continues to grow as ML workloads expand [@henderson2020towards]. Engineers can reduce this impact by selecting cloud regions powered by renewable energy\index{Renewable Energy!carbon reduction} (5$\times$ carbon reduction), applying model efficiency techniques (2–4$\times$ reduction through quantization), and scheduling intensive workloads during periods of abundant renewable energy.\index{Carbon-Aware Scheduling!renewable energy} +\index{Environmental Impact!carbon accounting}The TCO analysis above captures costs that appear on invoices, but computational resources carry costs that no invoice reflects. Environmental impact follows from computational efficiency: the same optimization techniques that reduce TCO also reduce carbon emissions. The optimization techniques from @sec-hardware-acceleration and @sec-model-compression reduce energy consumption per inference, directly lowering carbon footprint. Data centers consume an estimated 1–2% of global electricity\index{Data Centers!energy consumption}, a share that continues to grow as ML workloads expand [@henderson2020towards]. Engineers can reduce this impact by selecting cloud regions powered by renewable energy\index{Renewable Energy!carbon reduction} (5 $\times$ carbon reduction), applying model efficiency techniques (2–4 $\times$ reduction through quantization), and scheduling intensive workloads during periods of abundant renewable energy.\index{Carbon-Aware Scheduling!renewable energy} To appreciate the magnitude of these emissions, the following worked example quantifies *the carbon cost of scale* for training a large foundation model. @@ -1918,7 +1918,7 @@ Teams assume fairness constraints can be retrofitted once models demonstrate str **Pitfall:** *Relying on aggregate metrics to assess fairness.* -Engineers assume high overall accuracy indicates the system works well for all users. The Flaw of Averages (@sec-responsible-engineering-testing-across-populations-9f20) reveals this intuition fails: aggregate metrics conceal disparities exceeding 40$\times$ between demographic groups (@sec-responsible-engineering-testing-challenge-77b0). The loan approval analysis in @sec-responsible-engineering-worked-example-fairness-analysis-loan-approval-2c72 showed `{python} tpr_disparity_str` percentage point TPR gaps, meaning qualified minority applicants faced 4$\times$ higher rejection rates. These disparities persist for months undetected because standard monitoring tracks only aggregates. Production systems require disaggregated evaluation with alerts when subgroup disparity exceeds 1.25$\times$ error rate ratio or 5 percentage point TPR difference. +Engineers assume high overall accuracy indicates the system works well for all users. The Flaw of Averages (@sec-responsible-engineering-testing-across-populations-9f20) reveals this intuition fails: aggregate metrics conceal disparities exceeding 40 $\times$ between demographic groups (@sec-responsible-engineering-testing-challenge-77b0). The loan approval analysis in @sec-responsible-engineering-worked-example-fairness-analysis-loan-approval-2c72 showed `{python} tpr_disparity_str` percentage point TPR gaps, meaning qualified minority applicants faced 4 $\times$ higher rejection rates. These disparities persist for months undetected because standard monitoring tracks only aggregates. Production systems require disaggregated evaluation with alerts when subgroup disparity exceeds 1.25 $\times$ error rate ratio or 5 percentage point TPR difference. **Fallacy:** *Removing sensitive attributes from training data eliminates bias.* @@ -1926,11 +1926,11 @@ Teams remove gender, race, and protected attributes expecting this ensures fairn **Pitfall:** *Treating documentation as sufficient accountability.* -Teams invest effort in model cards, then consider responsibility requirements satisfied. Documentation provides transparency (@sec-responsible-engineering-model-documentation-standards-bef6) but not enforcement. Studies of model deployment patterns show 40--60% of production models operate outside their documented scope within 18 months. A model card specifying "not validated for high-stakes decisions" has no effect when the system is repurposed for loan approvals without technical restrictions. Accountability requires operational integration: monitoring dashboards, alert thresholds triggering at 1.25$\times$ subgroup disparity, incident response procedures, and access controls preventing deployment beyond validated use cases. +Teams invest effort in model cards, then consider responsibility requirements satisfied. Documentation provides transparency (@sec-responsible-engineering-model-documentation-standards-bef6) but not enforcement. Studies of model deployment patterns show 40--60% of production models operate outside their documented scope within 18 months. A model card specifying "not validated for high-stakes decisions" has no effect when the system is repurposed for loan approvals without technical restrictions. Accountability requires operational integration: monitoring dashboards, alert thresholds triggering at 1.25 $\times$ subgroup disparity, incident response procedures, and access controls preventing deployment beyond validated use cases. **Fallacy:** *Responsible AI is primarily a legal compliance issue.* -Teams treat responsibility as external oversight rather than engineering practice. Engineering decisions made months before legal review constrain the solution space more than any compliance assessment. Architecture selection determines what fairness interventions are feasible (adding demographic tracking to a 6-month-old pipeline costs 3–4$\times$ the initial implementation). Data pipeline design establishes whether disaggregated evaluation is even possible. As @sec-responsible-engineering-engineering-leadership-responsibility-e03c establishes, systems designed with responsibility as an engineering objective enable efficient validation; systems where responsibility is added at late-stage review face 6--12 months of redesign or deployment with documented risks. +Teams treat responsibility as external oversight rather than engineering practice. Engineering decisions made months before legal review constrain the solution space more than any compliance assessment. Architecture selection determines what fairness interventions are feasible (adding demographic tracking to a 6-month-old pipeline costs 3–4 $\times$ the initial implementation). Data pipeline design establishes whether disaggregated evaluation is even possible. As @sec-responsible-engineering-engineering-leadership-responsibility-e03c establishes, systems designed with responsibility as an engineering objective enable efficient validation; systems where responsibility is added at late-stage review face 6--12 months of redesign or deployment with documented risks. ## Summary {#sec-responsible-engineering-summary-45cf} @@ -1940,9 +1940,9 @@ The key insight unifying these tools is that responsibility concerns become trac ::: {.callout-takeaways title="Reliable for Whom?"} -* **Correctness is insufficient**\index{Correctness!insufficiency for responsible AI}: a model can achieve 95% accuracy while showing 43$\times$ error rate disparities across demographic groups. Aggregate metrics conceal failures that disaggregated, intersectional evaluation reveals. +* **Correctness is insufficient**\index{Correctness!insufficiency for responsible AI}: a model can achieve 95% accuracy while showing 43 $\times$ error rate disparities across demographic groups. Aggregate metrics conceal failures that disaggregated, intersectional evaluation reveals. * **Tractable responsibility**: "Fairness gap <5% across groups" is actionable; "be fair" is not. The Pareto frontier makes fairness-accuracy trade-offs explicit and quantifiable for stakeholder decisions. -* **Efficiency–responsibility alignment**\index{Efficiency!responsibility alignment}: a 4$\times$ more efficient model uses 4$\times$ less energy, costs 4$\times$ less, and enables 4$\times$ more organizations to deploy. Inference costs dominate TCO by 40:1 over training, making per-query optimization the highest-leverage responsibility intervention. +* **Efficiency–responsibility alignment**\index{Efficiency!responsibility alignment}: a 4 $\times$ more efficient model uses 4 $\times$ less energy, costs 4 $\times$ less, and enables 4 $\times$ more organizations to deploy. Inference costs dominate TCO by 40:1 over training, making per-query optimization the highest-leverage responsibility intervention. * **Checklist discipline**: the aviation-inspired checklist approach transforms abstract fairness concerns into concrete, phase-gated deployment questions that teams must answer before shipping. * **Proactive monitoring**\index{Silent Bias!proactive detection}: biased systems continue operating without alerts because degraded predictions look identical to normal predictions. Monitoring must track outcome distributions across demographic groups, not just aggregate accuracy. * **Governance as infrastructure**: data lineage, audit trails, access controls, and privacy-preserving techniques must be built into pipelines from inception. Regulations like GDPR impose specific technical capabilities (right to erasure, right to explanation) that cannot be retrofitted. diff --git a/book/quarto/contents/vol1/training/training.qmd b/book/quarto/contents/vol1/training/training.qmd index d205bcc7fe..0f347b7c9c 100644 --- a/book/quarto/contents/vol1/training/training.qmd +++ b/book/quarto/contents/vol1/training/training.qmd @@ -425,9 +425,9 @@ Three characteristics distinguish training workloads from general-purpose comput Each of these challenges opens a corresponding optimization pathway. Computational intensity can be addressed through hardware acceleration (@sec-hardware-acceleration) and precision reduction (using the BF16/FP8 formats discussed in @sec-model-compression). Memory pressure responds to techniques like gradient checkpointing[^fn-activation-checkpointing] (a specific application of *rematerialization*—discarding and recomputing intermediate values to save memory—from @sec-ml-frameworks), which trades recomputation for reduced activation storage, and mixed-precision training[^fn-mixed-precision], which halves the memory footprint of weights and activations. Data dependencies motivate pipeline designs that overlap computation with data movement, heavily relying on the data loading throughput optimized in @sec-data-engineering, so the GPU never sits idle waiting for the next batch. The current chapter focuses on single-machine and single-node multi-GPU training; scaling to hundreds of machines across network boundaries introduces communication and fault tolerance challenges beyond our current scope. -[^fn-activation-checkpointing]: **Activation Checkpointing (Gradient Checkpointing)**\index{Activation Checkpointing!etymology}: Introduced by Chen et al. [@chen2016training] in "Training Deep Nets with Sublinear Memory Cost." The term borrows from database systems, where "checkpointing" means saving state at intervals. In training, forward-pass activations must be stored for backpropagation, but deep networks can exhaust GPU memory. Checkpointing saves activations at strategic layer boundaries and recomputes the rest, trading roughly 33% extra compute for up to 10$\times$ memory reduction---enabling training of models that would otherwise not fit in memory. +[^fn-activation-checkpointing]: **Activation Checkpointing (Gradient Checkpointing)**\index{Activation Checkpointing!etymology}: Introduced by Chen et al. [@chen2016training] in "Training Deep Nets with Sublinear Memory Cost." The term borrows from database systems, where "checkpointing" means saving state at intervals. In training, forward-pass activations must be stored for backpropagation, but deep networks can exhaust GPU memory. Checkpointing saves activations at strategic layer boundaries and recomputes the rest, trading roughly 33% extra compute for up to 10 $\times$ memory reduction---enabling training of models that would otherwise not fit in memory. -[^fn-mixed-precision]: **Mixed-Precision Training**\index{Mixed-Precision Training!etymology}\index{BF16!Brain Floating Point origin}: Introduced by Micikevicius et al. [@micikevicius2018mixed] in a collaboration between NVIDIA and Baidu Research. Uses half-precision (FP16 or BF16) for computation while maintaining FP32 "master weights" for accumulation, where rounding errors would otherwise compound. A "loss scaling" trick prevents gradient underflow in FP16's limited dynamic range. The result: nearly 2$\times$ memory savings and 2--8$\times$ throughput gains on tensor cores. BF16 (Brain Floating Point, Google Brain) later simplified the technique by matching FP32's exponent range, eliminating loss scaling in most cases. +[^fn-mixed-precision]: **Mixed-Precision Training**\index{Mixed-Precision Training!etymology}\index{BF16!Brain Floating Point origin}: Introduced by Micikevicius et al. [@micikevicius2018mixed] in a collaboration between NVIDIA and Baidu Research. Uses half-precision (FP16 or BF16) for computation while maintaining FP32 "master weights" for accumulation, where rounding errors would otherwise compound. A "loss scaling" trick prevents gradient underflow in FP16's limited dynamic range. The result: nearly 2 $\times$ memory savings and 2--8 $\times$ throughput gains on tensor cores. BF16 (Brain Floating Point, Google Brain) later simplified the technique by matching FP32's exponent range, eliminating loss scaling in most cases. The chapter proceeds through five stages. First, we formalize the *Iron Law of Training Performance*, a specialized application of the general Iron Law (@sec-introduction-iron-law-ml-systems-c32a) that decomposes training time into total operations, peak throughput, and utilization—three levers that every optimization technique in this chapter targets. Second, we examine the *mathematical foundations* that underpin training: neural network computation as a systems workload, optimization algorithms that navigate loss landscapes, backpropagation mechanics, and the arithmetic intensity analysis that determines whether training is compute-bound or memory-bound. Third, we dissect the *pipeline architecture* of a training system—data loading, forward pass, backward pass, and parameter updates—as a staged pipeline where each component's throughput constrains the next. Fourth, we develop *pipeline optimizations*—mixed-precision training, FlashAttention, gradient accumulation, and checkpointing—that target specific Iron Law terms to close the gap between theoretical peak and actual training speed. Fifth, we explore how training *scales* beyond a single GPU to multi-GPU and multi-node configurations, where communication overhead introduces new bottlenecks. @@ -468,7 +468,7 @@ where $O$ (**Total Operations**) is the FLOPs required for one epoch times the n | **Technique** | **Term Affected** | **Mechanism** | |:--------------------------------|:----------------------------------|:-----------------------------------------------------------------------------------| -| **Mixed Precision (FP16/BF16)** | Peak Throughput ↑ | Tensor Cores operate at up to 16$\times$ higher FLOP/s | +| **Mixed Precision (FP16/BF16)** | Peak Throughput ↑ | Tensor Cores operate at up to 16 $\times$ higher FLOP/s | | **Data Prefetching** | Utilization ↑ | Reduces GPU idle time waiting for data | | **Gradient Checkpointing** | Total Operations ↑ | Adds recomputation, but enables larger models | | **Gradient Accumulation** | Utilization ↑ | Maintains high batch parallelism efficiency | @@ -479,7 +479,7 @@ where $O$ (**Total Operations**) is the FLOPs required for one epoch times the n A caveat: the Iron Law focuses on *execution efficiency*---how fast the hardware processes a given workload. It does not capture data-side factors such as data quality, dataset size, or curriculum design, which affect how many total operations $O$ are needed to reach a target accuracy. A cleaner dataset or a better data mix can reduce the number of epochs required, shrinking $O$ without touching hardware at all. These data-side levers are covered in @sec-data-engineering; here we hold the workload fixed and ask how to execute it as fast as possible. -The gap between theoretical peak performance and actual training speed is often 2–3$\times$. Scaling to multiple GPUs introduces additional communication overhead that can erode these gains---we examine this trade-off in @sec-model-training-scaling-training-systems-adfd. Before examining specific optimization techniques, verify your understanding of *why* this gap exists. +The gap between theoretical peak performance and actual training speed is often 2–3 $\times$. Scaling to multiple GPUs introduces additional communication overhead that can erode these gains---we examine this trade-off in @sec-model-training-scaling-training-systems-adfd. Before examining specific optimization techniques, verify your understanding of *why* this gap exists. ::: {.callout-checkpoint title="The Physics of Training" collapse="false"} Training speed is governed by the utilization of hardware peaks. @@ -491,11 +491,11 @@ Training speed is governed by the utilization of hardware peaks. **Precision Economics** -- [ ] **Mixed Precision**: How does FP16/BF16 double throughput? (Tensor Cores run 2$\times$ faster, and memory bandwidth effectively doubles). +- [ ] **Mixed Precision**: How does FP16/BF16 double throughput? (Tensor Cores run 2 $\times$ faster, and memory bandwidth effectively doubles). ::: \index{Transformer!training evolution} -The Iron Law provides a static framework for reasoning about training performance, but the history of deep learning reveals how the *binding constraint* has shifted over time as hardware and algorithms co-evolved. In 1986, backpropagation was formalized [@rumelhart1986learning], and training a 3-layer network on toy datasets required days on CPU workstations---the bottleneck was raw compute throughput ($R_{peak}$). In 2012, AlexNet demonstrated GPU training [@alexnet2012], reducing ImageNet training from weeks to days and launching the deep learning era. By 2017, Transformers and NVIDIA Volta Tensor Cores enabled mixed-precision training with a further 5$\times$ speedup [@vaswani2017attention]. GPT-3 in 2020 used over `{python} TrainingScenarios.gpt3_gpu_count_str` V100 GPUs at an estimated \$`{python} TrainingScenarios.gpt3_compute_cost_str`M cost [@brown2020language], making utilization ($\eta$) critical. By 2023, training efficiency improved 10$\times$ through the techniques examined in this chapter: FlashAttention reduces $O$ while improving $\eta$; gradient checkpointing trades $O$ for memory capacity; mixed precision increases $R_{peak}$. Each innovation was motivated by a specific Iron Law bottleneck. +The Iron Law provides a static framework for reasoning about training performance, but the history of deep learning reveals how the *binding constraint* has shifted over time as hardware and algorithms co-evolved. In 1986, backpropagation was formalized [@rumelhart1986learning], and training a 3-layer network on toy datasets required days on CPU workstations---the bottleneck was raw compute throughput ($R_{peak}$). In 2012, AlexNet demonstrated GPU training [@alexnet2012], reducing ImageNet training from weeks to days and launching the deep learning era. By 2017, Transformers and NVIDIA Volta Tensor Cores enabled mixed-precision training with a further 5 $\times$ speedup [@vaswani2017attention]. GPT-3 in 2020 used over `{python} TrainingScenarios.gpt3_gpu_count_str` V100 GPUs at an estimated \$`{python} TrainingScenarios.gpt3_compute_cost_str`M cost [@brown2020language], making utilization ($\eta$) critical. By 2023, training efficiency improved 10 $\times$ through the techniques examined in this chapter: FlashAttention reduces $O$ while improving $\eta$; gradient checkpointing trades $O$ for memory capacity; mixed precision increases $R_{peak}$. Each innovation was motivated by a specific Iron Law bottleneck. ### Running Example: Training GPT-2 {#sec-model-training-running-example-training-gpt2-19cd} @@ -577,7 +577,7 @@ The DL Primer established that forward propagation reduces to chains of matrix m \index{Matrix Multiplication!training operations}\index{Strassen's Algorithm!matrix multiplication}\index{cuBLAS!hardware-accelerated linear algebra} Matrix multiplication dominance has driven both algorithmic and hardware innovations. Early neural network implementations relied on standard CPU-based linear algebra libraries, but the scale of modern training demanded specialized optimizations. Strassen's algorithm[^fn-strassen-algorithm] reduced the naive $O(n^3)$ complexity to approximately $O(n^{2.81})$ [@strassen1969gauss], and contemporary hardware-accelerated libraries like cuBLAS [@nvidia_cublas] continue pushing computational efficiency limits. -[^fn-strassen-algorithm]: **Strassen's Algorithm**: Developed by Volker Strassen in 1969, this breakthrough reduced matrix multiplication from O(n³) to O(n^2.807) by using clever algebraic tricks with 7 multiplications instead of 8. While theoretically faster, it's only practical for matrices larger than 500$\times$500 due to overhead. Modern implementations in libraries like Intel MKL switch between algorithms based on matrix size, demonstrating how theoretical advances require careful engineering for practical impact. +[^fn-strassen-algorithm]: **Strassen's Algorithm**: Developed by Volker Strassen in 1969, this breakthrough reduced matrix multiplication from O(n³) to O(n^2.807) by using clever algebraic tricks with 7 multiplications instead of 8. While theoretically faster, it's only practical for matrices larger than 500 $\times$ 500 due to overhead. Modern implementations in libraries like Intel MKL switch between algorithms based on matrix size, demonstrating how theoretical advances require careful engineering for practical impact. This computational dominance has driven system-level optimizations: blocked matrix computations that parallelize across multiple units, and memory hierarchies designed for the access patterns of both forward and backward passes. As neural architectures grew, weight and activation matrices both had to remain accessible for backpropagation, and hardware evolved to serve these dense multiplication patterns within growing memory budgets. @@ -739,9 +739,9 @@ The progression from matrix-vector to batched matrix-matrix operations explains ::: {.callout-perspective title="Why GPUs Dominate Training" collapse="false"} -The matrix operations described above directly explain modern training hardware architecture. GPUs dominate training for three reasons. First, matrix multiplication's independent element calculations map perfectly to thousands of GPU cores (NVIDIA A100 has 6,912 CUDA cores). Second, specialized hardware units like Tensor Cores accelerate matrix operations by 10–20$\times$ through dedicated hardware for the dominant workload. Third, blocked matrix computation patterns enable efficient use of GPU memory hierarchy (L1/L2 cache, shared memory, global memory). +The matrix operations described above directly explain modern training hardware architecture. GPUs dominate training for three reasons. First, matrix multiplication's independent element calculations map perfectly to thousands of GPU cores (NVIDIA A100 has 6,912 CUDA cores). Second, specialized hardware units like Tensor Cores accelerate matrix operations by 10–20 $\times$ through dedicated hardware for the dominant workload. Third, blocked matrix computation patterns enable efficient use of GPU memory hierarchy (L1/L2 cache, shared memory, global memory). -When GPT-2 examples later show *why* V100 GPUs achieve 2.4$\times$ speedup with mixed precision, this acceleration comes from Tensor Cores executing the matrix multiplications we just analyzed. Matrix operation characteristics are prerequisite for appreciating *why* pipeline optimizations like mixed-precision training provide such substantial benefits. +When GPT-2 examples later show *why* V100 GPUs achieve 2.4 $\times$ speedup with mixed precision, this acceleration comes from Tensor Cores executing the matrix multiplications we just analyzed. Matrix operation characteristics are prerequisite for appreciating *why* pipeline optimizations like mixed-precision training provide such substantial benefits. ::: Matrix multiplications dominate training compute, but neural networks require more than linear transformations. Between each layer's matrix operations, activation functions introduce the nonlinearity that enables networks to learn complex patterns. These functions appear computationally trivial compared to matrix multiplication, yet their implementation characteristics affect training efficiency in ways that matter at scale. @@ -752,7 +752,7 @@ Matrix multiplications dominate training compute, but neural networks require mo The critical question for ML systems engineers is not *what* these functions do mathematically, but *how* to implement them efficiently at scale. This section analyzes the computational trade-offs that determine real-world training efficiency. -Because activation functions execute millions of times per training step, even small per-operation differences compound into significant training time impact. The selection of an activation function directly influences training throughput and hardware efficiency. @fig-activation-perf quantifies these performance differences through CPU benchmarks on Apple M2 hardware, revealing that Tanh executes in `{python} TrainingScenarios.tanh_exec_time_str` seconds compared to Sigmoid's `{python} TrainingScenarios.sigmoid_exec_time_str` seconds, a `{python} TrainingScenarios.tanh_speedup_str`$\times$ speedup. +Because activation functions execute millions of times per training step, even small per-operation differences compound into significant training time impact. The selection of an activation function directly influences training throughput and hardware efficiency. @fig-activation-perf quantifies these performance differences through CPU benchmarks on Apple M2 hardware, revealing that Tanh executes in `{python} TrainingScenarios.tanh_exec_time_str` seconds compared to Sigmoid's `{python} TrainingScenarios.sigmoid_exec_time_str` seconds, a `{python} TrainingScenarios.tanh_speedup_str` $\times$ speedup. ::: {#fig-activation-perf fig-env="figure" fig-pos="htb" fig-cap="**Activation Function Execution Time**: CPU benchmarks on Apple M2 hardware reveal significant variation: Tanh completes in `{python} TrainingScenarios.tanh_exec_time_str` seconds, ReLU in `{python} TrainingScenarios.relu_exec_time_str` seconds, Softmax in `{python} TrainingScenarios.softmax_exec_time_str` seconds, and Sigmoid in `{python} TrainingScenarios.sigmoid_exec_time_str` seconds. These differences directly affect training throughput and real-time inference latency, making activation function selection a system-level design decision." fig-alt="Bar chart comparing CPU execution times: Sigmoid at `{python} TrainingScenarios.sigmoid_exec_time_str` seconds, Tanh at `{python} TrainingScenarios.tanh_exec_time_str` seconds, ReLU at `{python} TrainingScenarios.relu_exec_time_str` seconds, and Softmax at `{python} TrainingScenarios.softmax_exec_time_str` seconds."} ```{.tikz} @@ -797,7 +797,7 @@ Because activation functions execute millions of times per training step, even s ::: \index{Sigmoid!computational cost}\index{Tanh!computational cost}\index{Sparsity!ReLU optimization}\index{Softmax!global normalization} -In production environments, modern hardware accelerators alter these relative characteristics, but the underlying cost hierarchy remains. Functions requiring transcendental operations are significantly more expensive than simple thresholding: in software, `exp()` takes `{python} TrainingScenarios.exp_cycles_min_str`--`{python} TrainingScenarios.exp_cycles_max_str` clock cycles compared to `{python} TrainingScenarios.arith_cycles_str` cycle for basic arithmetic. Modern GPUs and TPUs mitigate this through lookup tables or piece-wise linear approximations, but even optimized hardware-based sigmoid/tanh remains `{python} TrainingScenarios.sigmoid_slower_factor_min_str`--`{python} TrainingScenarios.sigmoid_slower_factor_max_str`$\times$ slower than ReLU. ReLU's $\max(0,x)$ requires only a single comparison and conditional set---a simple multiplexer checking the sign bit---enabling it to run at `{python} TrainingScenarios.relu_peak_flops_pct_str`%+ of peak FLOP/s, while sigmoid achieves only `{python} TrainingScenarios.sigmoid_peak_flops_pct_min_str`--`{python} TrainingScenarios.sigmoid_peak_flops_pct_max_str`% hardware utilization. Beyond raw throughput, ReLU's characteristic of producing roughly 50% zeros enables system-level sparsity optimizations---sparse matrix operations and gradient compression---that reduce memory bandwidth requirements, the primary bottleneck in large-scale training. In contrast, global normalization functions like Softmax[^fn-softmax-etymology-training] require access to the entire input vector simultaneously to compute the denominator, preventing the independent element-wise parallelization possible with Sigmoid or ReLU. +In production environments, modern hardware accelerators alter these relative characteristics, but the underlying cost hierarchy remains. Functions requiring transcendental operations are significantly more expensive than simple thresholding: in software, `exp()` takes `{python} TrainingScenarios.exp_cycles_min_str`--`{python} TrainingScenarios.exp_cycles_max_str` clock cycles compared to `{python} TrainingScenarios.arith_cycles_str` cycle for basic arithmetic. Modern GPUs and TPUs mitigate this through lookup tables or piece-wise linear approximations, but even optimized hardware-based sigmoid/tanh remains `{python} TrainingScenarios.sigmoid_slower_factor_min_str`--`{python} TrainingScenarios.sigmoid_slower_factor_max_str` $\times$ slower than ReLU. ReLU's $\max(0,x)$ requires only a single comparison and conditional set---a simple multiplexer checking the sign bit---enabling it to run at `{python} TrainingScenarios.relu_peak_flops_pct_str`%+ of peak FLOP/s, while sigmoid achieves only `{python} TrainingScenarios.sigmoid_peak_flops_pct_min_str`--`{python} TrainingScenarios.sigmoid_peak_flops_pct_max_str`% hardware utilization. Beyond raw throughput, ReLU's characteristic of producing roughly 50% zeros enables system-level sparsity optimizations---sparse matrix operations and gradient compression---that reduce memory bandwidth requirements, the primary bottleneck in large-scale training. In contrast, global normalization functions like Softmax[^fn-softmax-etymology-training] require access to the entire input vector simultaneously to compute the denominator, preventing the independent element-wise parallelization possible with Sigmoid or ReLU. [^fn-softmax-etymology-training]: **Softmax**: A "soft" (differentiable) approximation to the argmax function, returning a probability distribution rather than a hard one-hot vector. As introduced in @sec-neural-computation, its differentiability enables gradient-based learning for classification tasks. Its global normalization requirement creates unique memory access challenges for training systems. @@ -834,12 +834,12 @@ where $\Phi(x)$ is the cumulative distribution function of the standard normal d **System Performance Tradeoff** -- Computational cost: ~`{python} TrainingScenarios.gelu_cost_factor_min_str` to `{python} TrainingScenarios.gelu_cost_factor_max_str`$\times$ more expensive than ReLU (requires erf function evaluation) +- Computational cost: ~`{python} TrainingScenarios.gelu_cost_factor_min_str` to `{python} TrainingScenarios.gelu_cost_factor_max_str` $\times$ more expensive than ReLU (requires erf function evaluation) - Memory: Same as ReLU (element-wise operation) - Training time impact: For GPT-2's `{python} TrainingModels.gpt2_layers_str` layers, GELU adds ~`{python} TrainingScenarios.gelu_overhead_pct_min_str` to `{python} TrainingScenarios.gelu_overhead_pct_max_str`% to total forward pass time - Justified by results: The improved model quality (lower perplexity) offsets the computational overhead -Frameworks implement fast approximation of GELU using optimized formulas (@lst-gelu-approx). This approximation reduces computational cost to approximately `{python} TrainingScenarios.gelu_approx_cost_factor_str`$\times$ ReLU while maintaining GELU's benefits, demonstrating *how* production systems balance mathematical properties with implementation efficiency. +Frameworks implement fast approximation of GELU using optimized formulas (@lst-gelu-approx). This approximation reduces computational cost to approximately `{python} TrainingScenarios.gelu_approx_cost_factor_str` $\times$ ReLU while maintaining GELU's benefits, demonstrating *how* production systems balance mathematical properties with implementation efficiency. ::: @@ -859,9 +859,9 @@ The GELU approximation highlights a broader pattern: compute cost is not always ::: {.callout-perspective title="Memory Bandwidth Bottlenecks" collapse="false"} \index{Memory Bandwidth!activation function bottleneck} -Activation functions reveal a critical systems principle: not all operations are compute-bound. While matrix multiplications saturate GPU compute units, activation functions often become memory-bandwidth-bound for three reasons. First, element-wise operations perform few calculations per memory access (ReLU performs 1 operation per load). Second, simple operations complete faster than memory transfer time, limiting parallelism benefits. Third, modern GPUs have 10–100$\times$ more compute throughput than memory bandwidth. +Activation functions reveal a critical systems principle: not all operations are compute-bound. While matrix multiplications saturate GPU compute units, activation functions often become memory-bandwidth-bound for three reasons. First, element-wise operations perform few calculations per memory access (ReLU performs 1 operation per load). Second, simple operations complete faster than memory transfer time, limiting parallelism benefits. Third, modern GPUs have 10–100 $\times$ more compute throughput than memory bandwidth. -This is why activation function choice matters less than expected. ReLU versus sigmoid shows only 2–3$\times$ difference despite vastly different computational complexity, because both are bottlenecked by memory access. The forward pass must carefully manage activation storage to prevent memory bandwidth from limiting overall training throughput. +This is why activation function choice matters less than expected. ReLU versus sigmoid shows only 2–3 $\times$ difference despite vastly different computational complexity, because both are bottlenecked by memory access. The forward pass must carefully manage activation storage to prevent memory bandwidth from limiting overall training throughput. ::: Forward pass operations and their computational characteristics establish the workload that training systems must compute---matrix multiplications dominating FLOPs, activation functions constrained by memory bandwidth. But a neural network that only computes predictions learns nothing. Training requires updating model parameters so future predictions improve. The forward pass produces a loss value quantifying how wrong the current predictions are; the question now shifts from *how much does computation cost* to *how do we use the result to improve*. @@ -1048,7 +1048,7 @@ class AdamMemory: The system implications of Adam are more substantial than previous methods. The optimizer must store two additional vectors (m_t and v_t) for each parameter, tripling the memory required for optimization state. -[^fn-adam-optimizer-memory]: **Adam (Adaptive Moment Estimation)**: Introduced by Kingma and Ba in 2015, Adam became the default optimizer for deep learning due to its robust performance across diverse architectures. The algorithm maintains per-parameter learning rates using first and second moment estimates, requiring 3$\times$ the memory of SGD (parameters + two state vectors). For a `{python} TrainingScenarios.adam_7b_params_str`B model in FP32, this means `{python} TrainingScenarios.adam_7b_state_gb_str` GB for optimizer state alone, driving the adoption of memory-efficient variants like 8-bit Adam (`{python} TrainingScenarios.adam_7b_compression_str`$\times$ compression) and GaLoRE (gradient low-rank projection). +[^fn-adam-optimizer-memory]: **Adam (Adaptive Moment Estimation)**: Introduced by Kingma and Ba in 2015, Adam became the default optimizer for deep learning due to its robust performance across diverse architectures. The algorithm maintains per-parameter learning rates using first and second moment estimates, requiring 3 $\times$ the memory of SGD (parameters + two state vectors). For a `{python} TrainingScenarios.adam_7b_params_str`B model in FP32, this means `{python} TrainingScenarios.adam_7b_state_gb_str` GB for optimizer state alone, driving the adoption of memory-efficient variants like 8-bit Adam (`{python} TrainingScenarios.adam_7b_compression_str` $\times$ compression) and GaLoRE (gradient low-rank projection). #### Optimization Algorithm System Implications {#sec-model-training-optimization-algorithm-system-implications-f9f2} @@ -1160,7 +1160,7 @@ Adam's memory overhead is a necessary trade-off for convergence. GPT-2 converges ::: \index{AdamW!decoupled weight decay} -The costs quantified in @tbl-optimizer-properties create a design tension: Adam's 3$\times$ memory overhead buys faster convergence, but that overhead determines maximum feasible model size and batch size on a given GPU. Variants like AdamW [@loshchilov2019adamw] decouple weight decay from the gradient update, improving generalization without increasing memory cost. Training frameworks continue developing techniques like optimizer state sharding, mixed-precision storage, and fused operations to reduce the per-parameter overhead while preserving adaptive convergence benefits. +The costs quantified in @tbl-optimizer-properties create a design tension: Adam's 3 $\times$ memory overhead buys faster convergence, but that overhead determines maximum feasible model size and batch size on a given GPU. Variants like AdamW [@loshchilov2019adamw] decouple weight decay from the gradient update, improving generalization without increasing memory cost. Training frameworks continue developing techniques like optimizer state sharding, mixed-precision storage, and fused operations to reduce the per-parameter overhead while preserving adaptive convergence benefits. #### Framework Optimizer Interface and Scheduling {#sec-model-training-framework-optimizer-interface-82ff} @@ -1249,7 +1249,7 @@ for param in model.parameters(): ``` ::: -Framework implementations also handle the memory management challenges in optimizer trade-offs. The optimizer automatically allocates storage for momentum terms and squared gradient statistics, managing the 2–3$\times$ memory overhead transparently while providing efficient memory access patterns optimized for the underlying hardware. +Framework implementations also handle the memory management challenges in optimizer trade-offs. The optimizer automatically allocates storage for momentum terms and squared gradient statistics, managing the 2–3 $\times$ memory overhead transparently while providing efficient memory access patterns optimized for the underlying hardware. #### Learning Rate Scheduling Integration {#sec-model-training-learning-rate-scheduling-integration-451b} @@ -1461,8 +1461,8 @@ For GPT-2 with batch_size=`{python} GPT2ActivationMemory.batch_size_str`, seq_le **Per-Layer Activation Memory.** -- Attention activations: `batch × seq × hidden × 4` (Q, K, V, output) = `{python} GPT2ActivationMemory.batch_size_str` $\times$ `{python} GPT2Compute.seq_len_str` $\times$ `{python} GPT2ActivationMemory.hidden_dim_str` $\times$ 4 $\times$ 2 bytes (FP16) = `{python} GPT2ActivationMemory.attn_act_str` MB -- FFN activations: `batch × seq × (hidden × 4)` (intermediate expansion) = `{python} GPT2ActivationMemory.batch_size_str` $\times$ `{python} GPT2Compute.seq_len_str` $\times$ `{python} GPT2ActivationMemory.ffn_dim_str` $\times$ 2 bytes = `{python} GPT2ActivationMemory.ffn_act_str` MB +- Attention activations: `batch $\times$ seq $\times$ hidden $\times$ 4` (Q, K, V, output) = `{python} GPT2ActivationMemory.batch_size_str` $\times$ `{python} GPT2Compute.seq_len_str` $\times$ `{python} GPT2ActivationMemory.hidden_dim_str` $\times$ 4 $\times$ 2 bytes (FP16) = `{python} GPT2ActivationMemory.attn_act_str` MB +- FFN activations: `batch $\times$ seq $\times$ (hidden $\times$ 4)` (intermediate expansion) = `{python} GPT2ActivationMemory.batch_size_str` $\times$ `{python} GPT2Compute.seq_len_str` $\times$ `{python} GPT2ActivationMemory.ffn_dim_str` $\times$ 2 bytes = `{python} GPT2ActivationMemory.ffn_act_str` MB - Layer norm states: Minimal (~10 MB per layer) - Total per layer: ~`{python} GPT2ActivationMemory.per_layer_str` MB @@ -1494,7 +1494,7 @@ Training large models requires managing the memory wall (the bandwidth bottlenec **The Bottleneck** -- [ ] **Activation Memory**: Do you understand why activations (stored for backprop) dominate memory usage, often exceeding parameter size by 10$\times$? +- [ ] **Activation Memory**: Do you understand why activations (stored for backprop) dominate memory usage, often exceeding parameter size by 10 $\times$ ? - [ ] **Optimization Strategy**: Can you explain how **Gradient Checkpointing** trades compute (re-calculating activations) for memory capacity? **Scaling Limits** @@ -1658,7 +1658,7 @@ Batch size directly influences arithmetic intensity. With batch=1, many operatio This analysis guides optimization strategy selection. For memory-bound operations, reducing data movement through operator fusion, reduced precision, or algorithmic improvements like FlashAttention provides the largest gains. For compute-bound operations, increasing throughput through Tensor Cores, parallelism, or quantization matters more. See @sec-hardware-acceleration for detailed roofline model analysis and hardware-specific optimization strategies. -Look back at @fig-training-roofline and notice where standard attention sits (memory-bound region) versus where FlashAttention[^fn-flash-attention] lands (compute-bound region)—this shift represents the core insight of IO-aware algorithm design. By never materializing the full $N \times N$ attention matrix and instead processing in tiles that fit in fast SRAM (on-chip static RAM), FlashAttention reduces memory traffic from $O(N^2)$ to $O(N)$, achieving 2--4$\times$ speedups [@dao2022flashattention]. We examine the algorithm, its implementation, and when to use it in detail in @sec-model-training-flash-attention-ioaware-attention-optimization-3da0. +Look back at @fig-training-roofline and notice where standard attention sits (memory-bound region) versus where FlashAttention[^fn-flash-attention] lands (compute-bound region)—this shift represents the core insight of IO-aware algorithm design. By never materializing the full $N \times N$ attention matrix and instead processing in tiles that fit in fast SRAM (on-chip static RAM), FlashAttention reduces memory traffic from $O(N^2)$ to $O(N)$, achieving 2--4 $\times$ speedups [@dao2022flashattention]. We examine the algorithm, its implementation, and when to use it in detail in @sec-model-training-flash-attention-ioaware-attention-optimization-3da0. [^fn-flash-attention]: **FlashAttention**\index{FlashAttention!etymology}: Introduced by Tri Dao et al. [@dao2022flashattention] at Stanford (2022). The core innovation is *IO-awareness*: rather than optimizing FLOPs, FlashAttention optimizes *memory accesses* by tiling the computation to fit within GPU SRAM, avoiding materialization of the full $N \times N$ attention matrix in slower HBM. The algorithm performs slightly more FLOPs but dramatically fewer memory accesses, shifting attention from memory-bound to compute-bound. FlashAttention-2 [@dao2023flashattention2] further improved occupancy, achieving up to 72% of theoretical peak throughput. @@ -1675,49 +1675,51 @@ This section examines the *system-level pipeline* that coordinates these stages \index{Data Pipeline!ingestion and preprocessing}\index{Evaluation Pipeline!validation metrics} This orchestration is not a single monolithic process but rather three interconnected subsystems, each with distinct responsibilities and resource demands. Trace the flow through @fig-training-pipeline to see how these subsystems connect: the data pipeline handles ingestion and preprocessing, the training loop executes forward passes, backward passes, and parameter updates, and the evaluation pipeline periodically assesses model quality. Pay attention to how data flows between these components—the interconnection points are exactly where bottlenecks emerge. -```{python} -#| label: fig-training-pipeline -#| echo: false -#| fig-cap: "**Training System Overview**: Machine learning systems organize training through interconnected data, training, and evaluation pipelines. Data flows sequentially through these components, with evaluation metrics providing feedback to guide iterative model refinement and ensure reproducible results." -#| fig-alt: "Block diagram with three connected boxes: Data Pipeline, Training Loop, and Evaluation Pipeline. Arrows show data flow with feedback from evaluation." +::: {#fig-training-pipeline fig-env="figure" fig-pos="htb" fig-cap="**Training System Overview**: Machine learning systems organize training through interconnected data, training, and evaluation pipelines. Data flows sequentially through these components, with evaluation metrics providing feedback to guide iterative model refinement and ensure reproducible results." fig-alt="Block diagram with three connected boxes: Data Pipeline, Training Loop, and Evaluation Pipeline. Arrows show data flow from left to right, with a feedback arrow from evaluation back to training."} +```{.tikz} +\begin{tikzpicture}[ + font=\small\usefont{T1}{phv}{m}{n}, + box/.style={ + draw=BlueLine, + fill=BlueL, + rounded corners=2pt, + align=center, + minimum height=1.4cm, + minimum width=3.2cm, + line width=0.75pt, + font=\small\usefont{T1}{phv}{m}{n} + }, + arrow/.style={ + ->, + color=GrayLine, + line width=1.0pt, + >=latex + }, + label_text/.style={ + font=\footnotesize\usefont{T1}{phv}{m}{n}, + align=center, + color=TextBlack + }, + node distance=2.0cm +] -import matplotlib.patches as mpatches -from mlsys import viz +% Three pipeline boxes +\node[box] (data) {Data Pipeline\\\footnotesize\usefont{T1}{phv}{m}{n} Ingestion, Preprocessing,\\Batching}; +\node[box, right=of data] (train) {Training Loop\\\footnotesize\usefont{T1}{phv}{m}{n} Forward Pass, Loss,\\Backward Pass}; +\node[box, right=of train] (eval) {Evaluation Pipeline\\\footnotesize\usefont{T1}{phv}{m}{n} Validation and Metrics}; -fig, ax, COLORS, plt = viz.setup_plot(figsize=(10, 2.5)) -ax.set_xlim(-1, 13) -ax.set_ylim(-1.0, 1.5) -ax.set_aspect('equal') -ax.axis('off') -ax.grid(False) +% Data -> Training (top arrow) +\draw[arrow] (data.east) -- node[label_text, above=2pt] {Processed\\Batches} (train.west); -bw, bh = 2.8, 1.0 -arrow_kw = dict(arrowstyle='->', color='#555555', lw=1.5) +% Training -> Evaluation (top arrow, shifted up) +\draw[arrow] ([yshift=3pt]train.east) -- node[label_text, above=2pt] {Evaluation\\Metrics} ([yshift=3pt]eval.west); -# Three boxes -for x, title, sub in [(0, 'Data Pipeline', 'Ingestion, Preprocessing,\nBatching'), - (4.5, 'Training Loop', 'Forward Pass, Loss,\nBackward Pass'), - (9.5, 'Evaluation Pipeline', 'Validation and Metrics')]: - rect = mpatches.FancyBboxPatch((x - bw/2, -bh/2), bw, bh, boxstyle="round,pad=0.08", - facecolor=COLORS['BlueL'], edgecolor=COLORS['BlueLine'], linewidth=1.2, zorder=2) - ax.add_patch(rect) - ax.text(x, 0.15, title, ha='center', va='center', fontsize=7.5, fontweight='bold', zorder=3) - ax.text(x, -0.2, sub, ha='center', va='center', fontsize=6.5, zorder=3) +% Evaluation -> Training (bottom arrow, feedback) +\draw[arrow] ([yshift=-3pt]eval.west) -- node[label_text, below=2pt] {Feedback} ([yshift=-3pt]train.east); -# Data -> Training -ax.annotate('', xy=(3.1, 0.15), xytext=(1.4, 0.15), arrowprops=arrow_kw) -ax.text(2.25, 0.55, 'Processed\nBatches', ha='center', va='bottom', fontsize=6.5) - -# Training -> Evaluation (top arrow) -ax.annotate('', xy=(8.1, 0.25), xytext=(5.9, 0.25), arrowprops=arrow_kw) -ax.text(7, 0.6, 'Evaluation\nMetrics', ha='center', va='bottom', fontsize=6.5) - -# Evaluation -> Training (bottom arrow, feedback) -ax.annotate('', xy=(5.9, -0.15), xytext=(8.1, -0.15), arrowprops=arrow_kw) -ax.text(7, -0.55, 'Feedback', ha='center', va='top', fontsize=6.5) - -plt.show() +\end{tikzpicture} ``` +::: ### Architectural Overview {#sec-model-training-architectural-overview-5fc6} @@ -1743,7 +1745,7 @@ The data pipeline manages the ingestion, preprocessing, and batching of data for minimum height=0.8cm, minimum width=1.8cm, line width=1.0pt, - font=\small\bfseries\usefont{T1}{phv}{m}{n} + font=\footnotesize\usefont{T1}{phv}{m}{n} }, box_lg/.style 2 args={ box={#1}{#2}, @@ -1753,7 +1755,7 @@ The data pipeline manages the ingestion, preprocessing, and batching of data for ->, color=GrayLine, line width=1.2pt, - >=stealth + >=latex }, dash_arrow/.style={ arrow, @@ -1769,11 +1771,11 @@ The data pipeline manages the ingestion, preprocessing, and batching of data for % Row 2: Gradients \node[box_lg={RedL}{RedLine}, below=1.5cm of pred] (loss) {Loss Function\\(Error Calculation)}; -\node[box={GreenL}{GreenLine}, below=1.5cm of forward] (grads) {Parameter\\Gradients}; -\node[box_lg={BlueL}{BlueLine}, below=1.5cm of batch] (backward) {Backward Pass\\(Chain Rule)}; +\node[box_lg={BlueL}{BlueLine}, below=1.5cm of forward] (grads) {Backward Pass\\(Chain Rule)}; +\node[box={GreenL}{GreenLine}, below=1.5cm of batch] (backward) {Parameter\\Gradients}; \node[box={GreenL}{GreenLine}, right=of loss] (truth) {Ground\\Truth}; -\node[above=0.1cm of truth, font=\footnotesize\itshape, color=gray] {Labels}; +\node[above=0.1cm of truth, font=\footnotesize\usefont{T1}{phv}{m}{it}, color=gray] {Labels}; % Row 3: Update \node[box_lg={OrangeL}{OrangeLine}, below=1.5cm of backward] (optim) {Optimizer\\(Adam / SGD)}; @@ -1791,17 +1793,17 @@ The data pipeline manages the ingestion, preprocessing, and batching of data for % Loop back \draw[dash_arrow] (update.east) -- ++(0.5,0) coordinate(a) - -- (a |- optim.south) ++(0,-0.5) coordinate(b) % down below optim - -- (b -| backward.west) ++(-0.8,0) coordinate(c) % left of everything + -- (a |- optim.south) -- ++(0,-0.5) coordinate(b) % down below optim + -- (b -| backward.west) -- ++(-0.8,0) coordinate(c) % left of everything -- (c |- batch.west) -- (batch.west); % Step labels -\node[above=0.1cm of forward, font=\bfseries\small, color=TextBlack] {Step 1: Predict}; -\node[above=0.1cm of grads, font=\bfseries\small, color=TextBlack] {Step 2: Gradients}; -\node[above=0.1cm of optim, font=\bfseries\small, color=TextBlack, xshift=1.5cm] {Step 3: Update}; +\node[above=0.25cm of forward, font=\small\usefont{T1}{phv}{b}{n}, color=TextBlack] {Step 1: Predict}; +\node[above=0.25cm of grads, font=\small\usefont{T1}{phv}{b}{n}, color=TextBlack] {Step 2: Gradients}; +\node[above=0.25cm of optim, font=\small\usefont{T1}{phv}{b}{n}, color=TextBlack, xshift=1.5cm] {Step 3: Update}; % Next Iteration label -\node[left=0.1cm of b, font=\footnotesize\itshape, color=gray, anchor=east] {Next Iteration}; +\node[left=0.1cm of b, font=\footnotesize\usefont{T1}{phv}{m}{it}, color=gray, anchor=east] {Next Iteration}; \end{tikzpicture} ``` @@ -1842,7 +1844,7 @@ The data pipeline running on the CPU bridges raw data storage and GPU computatio align=center, inner sep=6pt, line width=0.8pt, - font=\bfseries\footnotesize, + font=\footnotesize\usefont{T1}{phv}{m}{n}, minimum height=0.8cm }, zone/.style={ @@ -1879,11 +1881,16 @@ The data pipeline running on the CPU bridges raw data storage and GPU computatio \draw[arrow] (batching.east) -- (gpu2.west); \draw[arrow] (batching.east) -- ++(0.5,0) |- (gpu3.west); +% Spacers for zone labels +\coordinate (storage_top) at ($(raw.north) + (0,0.4)$); +\coordinate (cpu_top) at ($(format.north) + (0,0.4)$); +\coordinate (gpu_top) at ($(gpu1.north) + (0,0.4)$); + % Zones \begin{scope}[on background layer] - \node[zone, fit=(raw), label={[anchor=north, font=\bfseries\scriptsize, yshift=-3pt]north:Storage Zone}] (storage_zone) {}; - \node[zone, fit=(format)(preprocess)(batching), label={[anchor=north, font=\bfseries\scriptsize, yshift=-3pt]north:CPU Preprocessing Zone}] (cpu_zone) {}; - \node[zone, fit=(gpu1)(gpu3), label={[anchor=north, font=\bfseries\scriptsize, yshift=-3pt]north:GPU Training Zone}] (gpu_zone) {}; + \node[zone, fit=(raw)(storage_top), label={[anchor=north, font=\scriptsize\usefont{T1}{phv}{b}{n}, yshift=-3pt]north:Storage Zone}] (storage_zone) {}; + \node[zone, fit=(format)(preprocess)(batching)(cpu_top), label={[anchor=north, font=\scriptsize\usefont{T1}{phv}{b}{n}, yshift=-3pt]north:CPU Preprocessing Zone}] (cpu_zone) {}; + \node[zone, fit=(gpu1)(gpu3)(gpu_top), inner xsep=20pt, label={[anchor=north, font=\scriptsize\usefont{T1}{phv}{b}{n}, yshift=-3pt]north:GPU Training Zone}] (gpu_zone) {}; \end{scope} % Data label @@ -2037,7 +2044,7 @@ System is balanced (tokenization ≈ GPU compute), but tokenization becomes bott - Multi-worker dataloading: `{python} GPT2DataPipeline.n_cpu_workers_str` CPU workers tokenize in parallel → `{python} GPT2DataPipeline.tokenization_ms_str` ms ÷ `{python} GPT2DataPipeline.n_cpu_workers_str` = `{python} GPT2DataPipeline.parallel_tokenization_ms_str` ms - Prefetching: Tokenize next batch while GPU processes current batch -- Result: GPU utilization >95%, training throughput: 380 samples/second on 8$\times$V100 +- Result: GPU utilization >95%, training throughput: 380 samples/second on 8 $\times$ V100 Text tokenization is CPU-bound (unlike image preprocessing which is I/O-bound). Language model training requires different pipeline optimizations than vision models. @@ -2132,9 +2139,9 @@ Training data traverses three memory tiers[^fn-memory-hierarchy-ml] on its way f $$R_{\text{memory}} =\min(BW_{\text{storage}}, BW_{\text{system}}, BW_{\text{accelerator}})$$ {#eq-memory-hierarchy-bandwidth} -[^fn-memory-hierarchy-ml]: **Memory Hierarchy in ML**: Unlike traditional CPU programs that focus on cache locality, ML training creates massive data flows between storage (TB datasets), system RAM (GB models), and GPU memory (GB activations). The 1000$\times$ bandwidth gap between storage (1-2 GB/s) and GPU memory (`{python} TrainingHardware.v100_bw_str`+ GB/s) forces ML systems to use sophisticated prefetching and caching strategies. Traditional cache optimization (spatial/temporal locality) is less relevant than managing bulk data transfers efficiently. See @sec-machine-foundations-memory-hierarchy-2278 for the full latency hierarchy and energy costs of data movement. +[^fn-memory-hierarchy-ml]: **Memory Hierarchy in ML**: Unlike traditional CPU programs that focus on cache locality, ML training creates massive data flows between storage (TB datasets), system RAM (GB models), and GPU memory (GB activations). The 1000 $\times$ bandwidth gap between storage (1-2 GB/s) and GPU memory (`{python} TrainingHardware.v100_bw_str`+ GB/s) forces ML systems to use sophisticated prefetching and caching strategies. Traditional cache optimization (spatial/temporal locality) is less relevant than managing bulk data transfers efficiently. See @sec-machine-foundations-memory-hierarchy-2278 for the full latency hierarchy and energy costs of data movement. -Storage devices provide 1--2 GB/s, system memory delivers 50--100 GB/s, and GPU HBM achieves `{python} TrainingHardware.v100_bw_str` GB/s or higher. Each jump represents roughly a 50–100$\times$ bandwidth increase, which means data that flows freely within GPU memory creates a severe bottleneck when it must be fetched from disk. This cascading bandwidth hierarchy explains why the iteration time of a well-pipelined system is governed by the *maximum* of its component latencies rather than their sum (@eq-iteration-time): +Storage devices provide 1--2 GB/s, system memory delivers 50--100 GB/s, and GPU HBM achieves `{python} TrainingHardware.v100_bw_str` GB/s or higher. Each jump represents roughly a 50–100 $\times$ bandwidth increase, which means data that flows freely within GPU memory creates a severe bottleneck when it must be fetched from disk. This cascading bandwidth hierarchy explains why the iteration time of a well-pipelined system is governed by the *maximum* of its component latencies rather than their sum (@eq-iteration-time): $$t_{\text{iteration}} =\max(t_{\text{fetch}}, t_{\text{process}}, t_{\text{transfer}})$$ {#eq-iteration-time} @@ -2171,7 +2178,7 @@ Modern neural architectures extend beyond these basic matrix operations to inclu \index{Attention Mechanism!similarity computation} Transformer architectures introduce attention mechanisms[^fn-attention-mechanisms], which compute similarity scores between sequences. These operations combine matrix multiplications with softmax normalization, requiring efficient broadcasting and reduction operations across varying sequence lengths. The computational pattern here differs significantly from convolutions, demanding flexible execution strategies from hardware accelerators. -[^fn-convolution]: **Convolutional Operations**: Sliding kernel operations applying learned filters across spatial dimensions to detect hierarchical features. A 3 $\times$ 3 convolution requires $9K^2$ multiplications for K-channel inputs; depthwise-separable variants (MobileNet) reduce this by 8–9$\times$. GPU implementations achieve >90% theoretical throughput through im2col matrix transformations, detailed in @sec-network-architectures. +[^fn-convolution]: **Convolutional Operations**: Sliding kernel operations applying learned filters across spatial dimensions to detect hierarchical features. A 3 $\times$ 3 convolution requires $9K^2$ multiplications for K-channel inputs; depthwise-separable variants (MobileNet) reduce this by 8–9 $\times$. GPU implementations achieve >90% theoretical throughput through im2col matrix transformations, detailed in @sec-network-architectures. [^fn-attention-mechanisms]: **Attention Mechanisms**: Dynamic weighting schemes enabling models to focus on relevant input regions. Introduced by Bahdanau et al. (2014) for machine translation, attention computes alignment scores between encoder/decoder states. Modern implementations include cross-attention (between sequences) and self-attention (within sequences), with softmax normalization ensuring weights sum to one. @@ -2192,12 +2199,12 @@ A common mistake in ML systems is treating batch size as a continuous variable. **Quantitative Example**: -| **Batch Size** | **Warps Needed** | **Utilization** | **Relative Time** | -|:------------------------------------------------|-----------------:|------------------------------------------------:|--------------------------------------------------------:| -| `{python} TrainingDimensions.wave_batch_32_str` | 1 | 100% | 1.0$\times$ | -| `{python} TrainingDimensions.wave_batch_33_str` | 2 | `{python} TrainingDimensions.wave_util_33_str`% | ~`{python} TrainingDimensions.wave_time_33_str`$\times$ | -| `{python} TrainingDimensions.wave_batch_64_str` | 2 | 100% | 1.0$\times$ | -| `{python} TrainingDimensions.wave_batch_65_str` | 3 | `{python} TrainingDimensions.wave_util_65_str`% | ~`{python} TrainingDimensions.wave_time_65_str`$\times$ | +| **Batch Size** | **Warps Needed** | **Utilization** | **Relative Time** | +|:------------------------------------------------|-----------------:|------------------------------------------------:|---------------------------------------------------------:| +| `{python} TrainingDimensions.wave_batch_32_str` | 1 | 100% | 1.0 $\times$ | +| `{python} TrainingDimensions.wave_batch_33_str` | 2 | `{python} TrainingDimensions.wave_util_33_str`% | ~`{python} TrainingDimensions.wave_time_33_str` $\times$ | +| `{python} TrainingDimensions.wave_batch_64_str` | 2 | 100% | 1.0 $\times$ | +| `{python} TrainingDimensions.wave_batch_65_str` | 3 | `{python} TrainingDimensions.wave_util_65_str`% | ~`{python} TrainingDimensions.wave_time_65_str` $\times$ | **Engineering Rule**: Always choose batch sizes and hidden dimensions that are powers of 2 or multiples of 8/32/64 to avoid this "quantization tax." A batch of 32 is often faster than 33, and a batch of 64 is often just as fast as 33. @@ -2299,7 +2306,7 @@ class VRAMRequirements: 5. **Activations**: Scale with batch size. Formula: Batch $\times$ SeqLen $\times$ Hidden $\times$ Layers $\times$ Bytes. Example: Batch=1, Seq=`{python} VRAMRequirements.vram_seq_str`, Hidden=`{python} VRAMRequirements.vram_hidden_str`, `{python} VRAMRequirements.vram_layers_str` Layers ≈ **`{python} VRAMRequirements.vram_activations_gb_str` GB** additional. \index{Parameter Sharding!memory constraint solution} -**The Systems Conclusion**: The "administrative tax" (gradients + optimizer states) is 4--6$\times$ larger than model weights. Training a `{python} VRAMRequirements.vram_params_b_str` B model on a single `{python} VRAMRequirements.vram_gpu_capacity_str` GB GPU requires quantization (4-bit) or parameter sharding (FSDP/ZeRO). +**The Systems Conclusion**: The "administrative tax" (gradients + optimizer states) is 4--6 $\times$ larger than model weights. Training a `{python} VRAMRequirements.vram_params_b_str` B model on a single `{python} VRAMRequirements.vram_gpu_capacity_str` GB GPU requires quantization (4-bit) or parameter sharding (FSDP/ZeRO). ::: The total memory scales linearly with batch size (as established in @eq-activation-memory-per-batch), which means the practical complexity lies not in the scaling law itself but in how these costs interact across layers. @@ -2503,8 +2510,8 @@ If you double the batch size, you perform half as many updates per epoch. If the ```{python} #| label: fig-linear-scaling-failure #| echo: false -#| fig-cap: "**The Linear Scaling Failure.** Training Loss vs. Steps (arbitrary units). Curve A (Blue) represents a standard baseline batch size. Curve B (Gray) shows what happens when batch size is increased 8× without tuning: convergence slows dramatically because weight updates are too infrequent. Curve C (Green) restores convergence by scaling the learning rate linearly (8× LR), allowing the model to take larger steps to compensate for fewer updates." -#| fig-alt: "Line chart of Loss vs Steps. Blue line (Baseline) converges fast. Gray line (Large Batch Naive) converges slow. Green line (Scaled LR) matches the baseline." +#| fig-cap: "**The Linear Scaling Failure.** Training Loss vs. Epochs (arbitrary units). Curve A (Blue) represents a standard baseline batch size. Curve B (Gray) shows what happens when batch size is increased 8× without tuning: convergence slows dramatically because weight updates are too infrequent (per epoch). Curve C (Green) restores convergence by scaling the learning rate linearly (8× LR), allowing the model to take larger steps to compensate for fewer updates." +#| fig-alt: "Line chart of Loss vs Epochs. Blue line (Baseline) converges fast. Gray line (Large Batch Naive) converges slow. Green line (Scaled LR) matches the baseline." import numpy as np from mlsys import viz @@ -2512,18 +2519,18 @@ from mlsys import viz fig, ax, COLORS, plt = viz.setup_plot() # --- Plot: The Linear Scaling Failure --- -steps = np.arange(0, 1000) -loss_base = 2.0 * np.exp(-0.01 * steps) + 0.2 -loss_large_naive = 2.0 * np.exp(-0.00125 * steps) + 0.2 -loss_large_scaled = 2.0 * np.exp(-0.009 * steps) + 0.25 +epochs = np.arange(0, 1000) +loss_base = 2.0 * np.exp(-0.01 * epochs) + 0.2 +loss_large_naive = 2.0 * np.exp(-0.00125 * epochs) + 0.2 +loss_large_scaled = 2.0 * np.exp(-0.009 * epochs) + 0.25 -ax.plot(steps, loss_base, '-', color=COLORS['BlueLine'], label='Batch 32') -ax.plot(steps, loss_large_naive, '--', color=COLORS['grid'], label='Batch 256 (Fixed LR)') -ax.plot(steps, loss_large_scaled, '-', color=COLORS['GreenLine'], label='Batch 256 (Scaled LR)') +ax.plot(epochs, loss_base, '-', color=COLORS['BlueLine'], label='Batch 32') +ax.plot(epochs, loss_large_naive, '--', color=COLORS['grid'], label='Batch 256 (Fixed LR)') +ax.plot(epochs, loss_large_scaled, '-', color=COLORS['GreenLine'], label='Batch 256 (Scaled LR)') ax.annotate("Optimization Gap", xy=(800, 0.5), xytext=(800, 1.5), arrowprops=dict(arrowstyle="<->", color=COLORS['RedLine']), bbox=dict(facecolor='white', alpha=0.8, edgecolor='none', pad=0.5)) -ax.set_xlabel('Steps') +ax.set_xlabel('Epochs') ax.set_ylabel('Loss (arb. units)') ax.legend(fontsize=8) plt.show() @@ -2741,109 +2748,97 @@ This systematic framework---profile, select, compose---applies to the four core Use @fig-optimization-flowchart as a decision tree to operationalize this systematic framework. Starting from profiling results, follow the branches through bottleneck identification to technique selection, ensuring optimization effort targets the actual constraint rather than perceived issues. -```{python} -#| label: fig-optimization-flowchart -#| echo: false -#| fig-cap: "**Training Optimization Decision Flowchart**: Systematic approach to optimization selection based on profiling results. Begin by measuring GPU utilization, then follow the decision path to identify whether the bottleneck is data-bound, memory-bound, or compute-bound. Each path leads to specific techniques that address the identified constraint." -#| fig-alt: "Flowchart showing optimization decision tree starting from Profile Training Run, branching based on GPU utilization and memory pressure to different optimization techniques." +::: {#fig-optimization-flowchart fig-env="figure" fig-pos="htb" fig-cap="**Training Optimization Decision Flowchart**: Systematic approach to optimization selection based on profiling results. Begin by measuring accelerator utilization, then follow the decision path to identify whether the bottleneck is data-bound, memory-bound, or compute-bound. Each path leads to specific techniques that address the identified constraint." fig-alt="Flowchart showing optimization decision tree starting from profile training run, branching by utilization and memory pressure toward data-bound, memory-bound, or compute-bound actions, then looping back to re-profile and iterate."} +```{.tikz} +\begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}, line width=0.8pt] +% Color palette aligned with chapter diagrams +\definecolor{BlueLine}{HTML}{006395} +\definecolor{BlueL}{HTML}{D1E6F3} +\definecolor{GreenLine}{HTML}{008F45} +\definecolor{GreenL}{HTML}{D4EFDF} +\definecolor{RedLine}{HTML}{CC0000} +\definecolor{RedL}{HTML}{F5D6D6} +\definecolor{VioletLine}{HTML}{702082} +\definecolor{VioletL}{HTML}{D8C2DE} -import matplotlib.patches as mpatches -import numpy as np -from mlsys import viz +\tikzset{ + Flow/.style={->, >=latex, draw=black!65, line width=0.85pt}, + StartStop/.style={ + draw=VioletLine, fill=VioletL, rounded corners=2.5pt, + minimum width=3.9cm, minimum height=0.9cm, align=center, font=\small\usefont{T1}{phv}{m}{n} + }, + Decision/.style={ + draw=BlueLine, fill=BlueL, diamond, aspect=2.2, + minimum width=3.6cm, minimum height=1.6cm, align=center, font=\small\usefont{T1}{phv}{m}{n} + }, + Bottleneck/.style={ + draw=GreenLine, fill=GreenL, rounded corners=2pt, + minimum width=3.2cm, minimum height=0.9cm, align=center, font=\small\usefont{T1}{phv}{m}{n} + }, + Action/.style={ + draw=RedLine, fill=RedL, rounded corners=2pt, + minimum width=4.2cm, minimum height=1.0cm, align=center, font=\small\usefont{T1}{phv}{m}{n} + }, + BranchLabel/.style={font=\scriptsize\usefont{T1}{phv}{m}{n}, fill=white, inner sep=1.2pt} +} -fig, ax, COLORS, plt = viz.setup_plot(figsize=(10, 10)) -ax.set_xlim(-7, 7) -ax.set_ylim(-11, 1) -ax.set_aspect('equal') -ax.axis('off') -ax.grid(False) +% Nodes +\node[StartStop] (profile) at (0,0) {Profile Training Run}; +\node[Decision] (util) at (0,-2.2) {Accelerator Util\\$<70\%$?}; -arrow_kw = dict(arrowstyle='->', color='#555555', lw=1.5) +% Data Branch (Left) +\node[Bottleneck] (data) at (-5.2,-3.9) {Data-Bound}; +\node[Action] (prefetch) at (-5.2,-5.3) {Apply Prefetching \&\\Pipeline Overlap}; -def rounded_box(cx, cy, w, h, text, fc, ec, rad="round,pad=0.15"): - rect = mpatches.FancyBboxPatch((cx - w/2, cy - h/2), w, h, boxstyle=rad, - facecolor=fc, edgecolor=ec, linewidth=1.2, zorder=2) - ax.add_patch(rect) - ax.text(cx, cy, text, ha='center', va='center', fontsize=9, fontweight='bold', zorder=3) +% Memory Check (Center) +\node[Decision] (memq) at (0,-6.2) {OOM Errors or\\Mem $>90\%$?}; -def diamond(cx, cy, w, h, text, fc, ec): - verts = [(cx, cy+h/2), (cx+w/2, cy), (cx, cy-h/2), (cx-w/2, cy), (cx, cy+h/2)] - d = plt.Polygon(verts, facecolor=fc, edgecolor=ec, linewidth=1.2, zorder=2) - ax.add_patch(d) - ax.text(cx, cy, text, ha='center', va='center', fontsize=8, fontweight='bold', zorder=3) +% Memory Branch (Left-Center) +\node[Bottleneck] (memb) at (-5.2,-8.2) {Memory-Bound}; +\node[Action] (memact) at (-5.2,-9.7) {Mixed Precision,\\Checkpointing,\\Accumulation}; -# Start: Profile Training Run -rounded_box(0, 0, 3.2, 0.8, 'Profile Training Run', '#F0D0F0', COLORS['GreenLine'], "round,pad=0.2") +% Compute Branch (Right) +\node[Bottleneck] (compb) at (5.2,-8.2) {Compute-Bound}; +\node[Action] (compact) at (5.2,-9.7) {Increase Batch Size,\\Optimize Kernels}; -# Decision: GPU Util < 70%? -diamond(0, -2, 3.5, 1.8, 'GPU Util\n< 70%?', COLORS['BlueL'], COLORS['BlueLine']) +% End Node +\node[StartStop] (reprofile) at (0,-11.8) {Re-profile \& Iterate}; -# Data-Bound path (left) -rounded_box(-4.5, -3.8, 2.5, 0.7, 'Data-Bound', COLORS['GreenL'], COLORS['GreenLine']) -rounded_box(-4.5, -5.0, 3.0, 0.8, 'Apply Prefetching &\nPipeline Overlap', COLORS['RedL'], COLORS['RedLine']) +% Main flow connections +\draw[Flow] (profile.south) -- (util.north); +\draw[Flow] (util.west) -| (data.north) node[pos=0.25, BranchLabel] {Yes}; +\draw[Flow] (util.south) -- (memq.north) node[pos=0.55, right, font=\scriptsize] {No}; +\draw[Flow] (data.south) -- (prefetch.north); -# Decision: Memory -diamond(0, -5, 3.5, 1.8, 'OOM Errors or\nMem > 90%?', COLORS['BlueL'], COLORS['BlueLine']) +\draw[Flow] (memq.west) -| (memb.north) node[pos=0.25, BranchLabel] {Yes}; +\draw[Flow] (memq.east) -| (compb.north) node[pos=0.25, BranchLabel] {No}; +\draw[Flow] (memb.south) -- (memact.north); +\draw[Flow] (compb.south) -- (compact.north); -# Memory-Bound path (left) -rounded_box(-4.5, -7, 2.5, 0.7, 'Memory-Bound', COLORS['GreenL'], COLORS['GreenLine']) -rounded_box(-4.5, -8.5, 3.0, 1.0, 'Mixed Precision,\nCheckpointing,\nAccumulation', COLORS['RedL'], COLORS['RedLine']) +% Feedback Loop Bus +\coordinate (bus_y) at (0,-11.2); +\coordinate (bus_left_end) at (-8.5, -11.2); +\coordinate (bus_right_end) at (5.2, -11.2); -# Compute-Bound path (right) -rounded_box(4.5, -7, 2.5, 0.7, 'Compute-Bound', COLORS['GreenL'], COLORS['GreenLine']) -rounded_box(4.5, -8.5, 3.0, 0.8, 'Increase Batch Size,\nOptimize Kernels', COLORS['RedL'], COLORS['RedLine']) +% Draw Bus Line +\draw[draw=black!65, line width=0.85pt] (bus_left_end) -- (bus_right_end); -# Re-profile & Iterate -rounded_box(0, -10.2, 3.2, 0.8, 'Re-profile & Iterate', '#F0D0F0', COLORS['GreenLine'], "round,pad=0.2") +% Connect Actions to Bus +% 1. Prefetch: Route around memory branch to the far left +\draw[Flow] (prefetch.west) -- ++(-0.8,0) |- (bus_y); -# --- Arrows --- -# Profile -> GPU decision -ax.annotate('', xy=(0, -1.1), xytext=(0, -0.4), arrowprops=arrow_kw) +% 2. Memory Action: Straight down +\draw[Flow] (memact.south) -- (memact.south |- bus_y); -# GPU -> Data-Bound (Yes, left) -ax.annotate('', xy=(-4.5, -3.45), xytext=(-1.75, -2), arrowprops=arrow_kw) -ax.text(-3.5, -2.5, 'Yes', ha='center', fontsize=8, bbox=dict(facecolor='#F8F9FA', edgecolor='none', pad=2)) +% 3. Compute Action: Straight down +\draw[Flow] (compact.south) -- (compact.south |- bus_y); -# GPU -> Memory decision (No, down) -ax.annotate('', xy=(0, -4.1), xytext=(0, -2.9), arrowprops=arrow_kw) -ax.text(0.4, -3.5, 'No', ha='left', fontsize=8, bbox=dict(facecolor='#F8F9FA', edgecolor='none', pad=2)) +% Connect Bus to Reprofile +\draw[Flow] (bus_y -| reprofile.north) -- (reprofile.north); -# Data-Bound -> Prefetch -ax.annotate('', xy=(-4.5, -4.6), xytext=(-4.5, -4.15), arrowprops=arrow_kw) - -# Memory -> Memory-Bound (Yes, left) -ax.annotate('', xy=(-4.5, -6.65), xytext=(-1.75, -5), arrowprops=arrow_kw) -ax.text(-3.5, -5.6, 'Yes', ha='center', fontsize=8, bbox=dict(facecolor='#F8F9FA', edgecolor='none', pad=2)) - -# Memory -> Compute-Bound (No, right) -ax.annotate('', xy=(4.5, -6.65), xytext=(1.75, -5), arrowprops=arrow_kw) -ax.text(3.5, -5.6, 'No', ha='center', fontsize=8, bbox=dict(facecolor='#F8F9FA', edgecolor='none', pad=2)) - -# Bottleneck -> Action -ax.annotate('', xy=(-4.5, -8.0), xytext=(-4.5, -7.35), arrowprops=arrow_kw) -ax.annotate('', xy=(4.5, -8.1), xytext=(4.5, -7.35), arrowprops=arrow_kw) - -# Feedback loops to Re-profile — single shared bus -fb_y = -9.8 # shared horizontal routing level - -# All three action boxes drop down to fb_y, then a single arrow goes up to Re-profile -# Prefetch (left path) drops straight down -ax.plot([-6, -6], [-5.4, fb_y], color='#555555', lw=1.3, zorder=1) - -# Memory tech drops down to fb_y -ax.plot([-4.5, -4.5], [-9.0, fb_y], color='#555555', lw=1.3, zorder=1) - -# Compute tech drops down to fb_y -ax.plot([4.5, 4.5], [-8.9, fb_y], color='#555555', lw=1.3, zorder=1) - -# Shared horizontal bus connecting all three drop points -ax.plot([-6, 4.5], [fb_y, fb_y], color='#555555', lw=1.3, zorder=1) - -# Single arrow from bus center up to Re-profile box -ax.annotate('', xy=(0, -10.6), xytext=(0, fb_y), arrowprops=arrow_kw) - -plt.show() +\end{tikzpicture} ``` +::: The flowchart embodies a critical insight: optimization is iterative. After applying a technique, re-profiling often reveals that a different bottleneck has become dominant. A data-bound system that implements prefetching may become memory-bound, requiring the next technique in the decision tree. This iterative refinement continues until profiling shows balanced resource utilization or acceptable training throughput. @@ -3043,18 +3038,18 @@ A neural network trained in FP32 requires `{python} ResNetMemoryScaling.bytes_fp \index{BF16!exponent range preservation} The numerical precision differences between these formats shape their use cases. @tbl-precision-comparison reveals that BF16's 8-bit exponent matches FP32's dynamic range ($10^{-45}$ minimum representable), while FP16's 5-bit exponent limits its range to $6 \times 10^{-8}$, explaining why gradients below this threshold underflow to zero without loss scaling. FP32 represents numbers from approximately $\pm1.18 \times 10^{-38}$ to $\pm3.4 \times 10^{38}$ with 7 decimal digits of precision. FP16 ranges from $\pm6.10 \times 10^{-5}$ to $\pm65,504$ with 3-4 decimal digits of precision. Bfloat16, developed by Google Brain, maintains the same dynamic range as FP32 ($\pm1.18 \times 10^{-38}$ to $\pm3.4 \times 10^{38}$) but with reduced precision (3-4 decimal digits). This range preservation makes bfloat16 particularly suited for deep learning training, as it handles large and small gradients more effectively than FP16. -| **Property** | **FP32** | **FP16** | **BF16** | -|:------------------------|-----------:|---------------------:|-----------:| -| **Exponent bits** | 8 | 5 | 8 | -| **Mantissa bits** | 23 | 10 | 7 | -| **Min normal value** | $10^{-38}$ | $6.1 \times 10^{-5}$ | $10^{-38}$ | -| **Tensor Core speedup** | 1$\times$ | 16$\times$ | 16$\times$ | +| **Property** | **FP32** | **FP16** | **BF16** | +|:------------------------|-----------:|---------------------:|------------:| +| **Exponent bits** | 8 | 5 | 8 | +| **Mantissa bits** | 23 | 10 | 7 | +| **Min normal value** | $10^{-38}$ | $6.1 \times 10^{-5}$ | $10^{-38}$ | +| **Tensor Core speedup** | 1 $\times$ | 16 $\times$ | 16 $\times$ | : **Precision Format Comparison.** The choice between FP16 and BF16 depends on whether dynamic range (BF16's strength) or precision (FP16's advantage) matters more for the specific workload. Minimum normal values shown are the practical thresholds for training, as subnormal values may flush to zero on many GPUs. {#tbl-precision-comparison} The choice between formats depends on model characteristics. Models with gradient outliers, common in transformer architectures, generally benefit from BF16's wider dynamic range. Models with well-conditioned gradients may prefer FP16's greater mantissa precision. Regardless of the reduced-precision format chosen for forward and backward passes, certain operations require FP32 precision: loss accumulation, softmax denominators, normalization variance computation, and optimizer state. These requirements stem from the numerical sensitivity of these operations rather than arbitrary convention. -@fig-mixed-precision traces the data flow through mixed-precision training's six-step cycle: FP32 master weights convert to FP16 for the forward pass (step 1), the forward pass computes FP16 loss (step 2), loss is scaled to prevent gradient underflow (step 3), backpropagation computes scaled FP16 gradients (step 4), gradients are copied to FP32 and unscaled (step 5), and FP32 gradients update the master weights (step 6), completing the cycle that achieves 16$\times$ Tensor Core speedup while preserving numerical stability through strategic precision management. +@fig-mixed-precision traces the data flow through mixed-precision training's six-step cycle: FP32 master weights convert to FP16 for the forward pass (step 1), the forward pass computes FP16 loss (step 2), loss is scaled to prevent gradient underflow (step 3), backpropagation computes scaled FP16 gradients (step 4), gradients are copied to FP32 and unscaled (step 5), and FP32 gradients update the master weights (step 6), completing the cycle that achieves 16 $\times$ Tensor Core speedup while preserving numerical stability through strategic precision management. ::: {#fig-mixed-precision fig-env="figure" fig-pos="htb" fig-cap="**Mixed Precision Training**: The six-step cycle: (1) FP32 master weights cast to FP16, (2) forward pass computes FP16 loss, (3) loss is scaled to prevent gradient underflow, (4) backpropagation computes scaled FP16 gradients, (5) gradients are copied to FP32 and unscaled, and (6) FP32 gradients update master weights. This approach achieves Tensor Core speedups while preserving numerical stability." fig-alt="Flowchart showing 6-step mixed precision training cycle. FP32 master weights convert to FP16 for forward pass, loss scaling protects gradients during backpropagation, then gradients update FP32 weights."} ```{.tikz} @@ -3127,13 +3122,13 @@ FP16 is efficient, but its limited precision can lead to numerical instability i \index{Loss Scaling!gradient underflow prevention}\index{Mixed Precision Training!loss scaling}\index{Automatic Mixed Precision!framework support} One of the key challenges with FP16 is its reduced dynamic range[^fn-fp16-range], which increases the likelihood of gradient values becoming too small to be represented accurately. Loss scaling addresses this issue by temporarily amplifying gradient values during backpropagation. Specifically, the loss value is scaled by a large factor (e.g., $2^{10}$) before gradients are computed, ensuring they remain within the representable range of FP16. -[^fn-fp16-range]: **FP16 Dynamic Range**: IEEE 754 half-precision (FP16) has only 5 exponent bits vs. 8 in FP32, limiting its range to ±65,504 (vs. ±3.4$\times$10³⁸ for FP32). More critically, FP16's smallest representable positive number is 6$\times$10⁻⁸, while gradients in deep networks often fall below 10⁻¹⁰. This mismatch causes gradient underflow, where tiny but important gradients become zero, stalling training, hence the need for loss scaling techniques. Once the gradients are computed, the scaling factor is reversed during the weight update step to restore the original gradient magnitude. This process allows FP16 to be used effectively without sacrificing numerical stability. +[^fn-fp16-range]: **FP16 Dynamic Range**: IEEE 754 half-precision (FP16) has only 5 exponent bits vs. 8 in FP32, limiting its range to ±65,504 (vs. ±3.4 $\times$ 10³⁸ for FP32). More critically, FP16's smallest representable positive number is 6 $\times$ 10⁻⁸, while gradients in deep networks often fall below 10⁻¹⁰. This mismatch causes gradient underflow, where tiny but important gradients become zero, stalling training, hence the need for loss scaling techniques. Once the gradients are computed, the scaling factor is reversed during the weight update step to restore the original gradient magnitude. This process allows FP16 to be used effectively without sacrificing numerical stability. Machine learning frameworks provide built-in support for mixed-precision training\index{Mixed Precision Training!FP16/BF16}. PyTorch's `torch.cuda.amp` (Automatic Mixed Precision) library automates the process of selecting which operations to perform in FP16 or FP32, as well as applying loss scaling when necessary. #### Mixed-Precision Benefits {#sec-model-training-mixedprecision-benefits-d57b} -Mixed-precision benefits manifest across three dimensions that compound in practice. First, memory consumption decreases by approximately `{python} TrainingScenarios.mp_mem_savings_pct_str`%: a 1 billion parameter transformer requires `{python} TrainingScenarios.model_1b_fp32_gb_str` GB in FP32 but only `{python} TrainingScenarios.model_1b_fp16_gb_str` GB in FP16 for weights alone, enabling larger batch sizes or deeper architectures. Second, computational throughput increases dramatically as Tensor Cores achieve 2–3$\times$ speedup for matrix multiplications, as detailed in @sec-model-training-mixedprecision-hardware-support-d7c1. Third, halving tensor sizes proportionally reduces inter-device communication bandwidth requirements in distributed training. +Mixed-precision benefits manifest across three dimensions that compound in practice. First, memory consumption decreases by approximately `{python} TrainingScenarios.mp_mem_savings_pct_str`%: a 1 billion parameter transformer requires `{python} TrainingScenarios.model_1b_fp32_gb_str` GB in FP32 but only `{python} TrainingScenarios.model_1b_fp16_gb_str` GB in FP16 for weights alone, enabling larger batch sizes or deeper architectures. Second, computational throughput increases dramatically as Tensor Cores achieve 2–3 $\times$ speedup for matrix multiplications, as detailed in @sec-model-training-mixedprecision-hardware-support-d7c1. Third, halving tensor sizes proportionally reduces inter-device communication bandwidth requirements in distributed training. These benefits compound: a practitioner might simultaneously double batch size (memory savings), accelerate each iteration (Tensor Core throughput), and reduce gradient synchronization time (smaller tensors). Quantifying the *GPT-2 mixed precision training impact* makes these compounding gains concrete. @@ -3337,7 +3332,7 @@ On NVIDIA V100 (Tensor Cores enabled): - FP32 throughput: ~`{python} v100_fp32_samples` samples/sec - FP16 throughput: ~`{python} v100_fp16_samples` samples/sec -- Speedup: `{python} v100_mp_speedup_str`$\times$ faster training +- Speedup: `{python} v100_mp_speedup_str` $\times$ faster training **Critical Implementation Details** @@ -3384,18 +3379,18 @@ where $A$ and $B$ are $4 \times 4$ FP16 matrices, $C$ is an FP32 accumulator, an The computational advantage of Tensor Cores becomes apparent when comparing theoretical peak performance across precisions. An NVIDIA A100 GPU specifications: - **FP32 throughput**: `{python} TrainingHardware.a100_tflops_fp32_str` TFLOPS (standard CUDA cores) -- **FP16 Tensor Core throughput**: `{python} TrainingHardware.a100_tflops_fp16_str` TFLOPS (16$\times$ speedup) +- **FP16 Tensor Core throughput**: `{python} TrainingHardware.a100_tflops_fp16_str` TFLOPS (16 $\times$ speedup) - **BF16 Tensor Core throughput**: `{python} TrainingHardware.a100_tflops_fp16_str` TFLOPS (same as FP16) -- **FP8 Tensor Core throughput** (H100 SXM): `{python} TrainingHardware.h100_tflops_fp8_str` TFLOPS without sparsity (approximately 100$\times$ speedup over FP32) +- **FP8 Tensor Core throughput** (H100 SXM): `{python} TrainingHardware.h100_tflops_fp8_str` TFLOPS without sparsity (approximately 100 $\times$ speedup over FP32) -This 16$\times$ theoretical speedup for FP16 materializes in practice because matrix multiplications, the dominant operation in neural network training, map naturally to Tensor Core operations. A transformer's attention mechanism computing $QK^T$ for a $(B, H, N, D)$ tensor requires $2 \times B \times H \times N^2 \times D$ FLOPs. On Tensor Cores, this executes 16$\times$ faster than on CUDA cores, directly translating to wall-clock speedups. +This 16 $\times$ theoretical speedup for FP16 materializes in practice because matrix multiplications, the dominant operation in neural network training, map naturally to Tensor Core operations. A transformer's attention mechanism computing $QK^T$ for a $(B, H, N, D)$ tensor requires $2 \times B \times H \times N^2 \times D$ FLOPs. On Tensor Cores, this executes 16 $\times$ faster than on CUDA cores, directly translating to wall-clock speedups. ##### BF16 Hardware Implementation {#sec-model-training-bf16-hardware-implementation-78f3} \index{BF16!dynamic range preservation}\index{BF16!hardware support} Brain Float 16 (BF16) maintains FP32's 8-bit exponent while reducing the mantissa to 7 bits. This design choice prioritizes dynamic range preservation over precision, which matters for gradient-based learning where values span many orders of magnitude. Google's TPUs natively support BF16, while NVIDIA's Ampere architecture (A100) and newer provide full hardware support. -The hardware advantage of BF16 over FP16 emerges in gradient accumulation scenarios. Consider summing 1000 gradients with values around $10^{-4}$. FP16's smallest positive subnormal value is approximately $6 \times 10^{-8}$, but the smallest normal value is $6.1 \times 10^{-5}$.[^fn-fp16-subnormal] In practice, gradients below approximately $10^{-7}$ may underflow to zero depending on hardware behavior. BF16's smallest representable value matches FP32 at approximately $10^{-45}$, so no underflow occurs. FP32 has full range but computes 2$\times$ slower. +The hardware advantage of BF16 over FP16 emerges in gradient accumulation scenarios. Consider summing 1000 gradients with values around $10^{-4}$. FP16's smallest positive subnormal value is approximately $6 \times 10^{-8}$, but the smallest normal value is $6.1 \times 10^{-5}$.[^fn-fp16-subnormal] In practice, gradients below approximately $10^{-7}$ may underflow to zero depending on hardware behavior. BF16's smallest representable value matches FP32 at approximately $10^{-45}$, so no underflow occurs. FP32 has full range but computes 2 $\times$ slower. [^fn-fp16-subnormal]: Many GPU implementations flush subnormal numbers to zero for performance reasons, making the normal minimum ($6.1 \times 10^{-5}$) the practical threshold. Loss scaling addresses this by multiplying gradients before the backward pass to keep values in the representable range. @@ -3469,7 +3464,7 @@ Optimal mixed-precision training requires matching the precision format to hardw | **Architecture** | **Recommended Precision** | **Key Considerations** | |:------------------|-------------------------------------:|------------------------------------------------------------------------------------------------------------:| | **V100 (Volta)** | FP16 with loss scaling | No BF16 support; gradient clipping essential | -| **A100 (Ampere)** | BF16 for transformers; FP16 for CNNs | TF32 mode provides automatic 2--3$\times$ speedup for legacy FP32 code | +| **A100 (Ampere)** | BF16 for transformers; FP16 for CNNs | TF32 mode provides automatic 2--3 $\times$ speedup for legacy FP32 code | | **H100 (Hopper)** | FP8 via TransformerEngine | Requires FP8-aware training recipes; `{python} TrainingHardware.h100_tflops_fp8_str` TFLOPS peak throughput | : **Precision Strategy by GPU Architecture.** Each generation introduces wider precision support, reducing the engineering burden of loss scaling while increasing throughput. {#tbl-hw-precision-strategy} @@ -3510,13 +3505,13 @@ a100_over_v100_str = fmt(a100_over_v100, precision=1, commas=False) h100_over_v100_str = fmt(h100_over_v100, precision=0, commas=False) ``` -The performance impact across generations is substantial. Training our lighthouse GPT-2 model (`{python} TrainingModels.gpt2_params_b_str` B parameters) on a single GPU illustrates how hardware and precision co-evolve: V100 achieves `{python} v100_fp32_sps` samples/sec in FP32 and `{python} v100_fp16_sps` samples/sec in FP16 (`{python} v100_fp16_speedup_str`$\times$ speedup), A100 reaches `{python} a100_bf16_sps` samples/sec in BF16 (`{python} a100_over_v100_str`$\times$ over V100 FP32), and H100 delivers `{python} h100_fp8_sps` samples/sec in FP8 (`{python} h100_over_v100_str`$\times$ over V100 FP32). These speedups compound with the memory savings discussed earlier, enabling both faster iteration and larger models. The hardware-software co-design principle emerges clearly: algorithmic techniques like mixed precision unlock specialized hardware capabilities, while hardware features like Tensor Cores make certain algorithms practical. +The performance impact across generations is substantial. Training our lighthouse GPT-2 model (`{python} TrainingModels.gpt2_params_b_str` B parameters) on a single GPU illustrates how hardware and precision co-evolve: V100 achieves `{python} v100_fp32_sps` samples/sec in FP32 and `{python} v100_fp16_sps` samples/sec in FP16 (`{python} v100_fp16_speedup_str` $\times$ speedup), A100 reaches `{python} a100_bf16_sps` samples/sec in BF16 (`{python} a100_over_v100_str` $\times$ over V100 FP32), and H100 delivers `{python} h100_fp8_sps` samples/sec in FP8 (`{python} h100_over_v100_str` $\times$ over V100 FP32). These speedups compound with the memory savings discussed earlier, enabling both faster iteration and larger models. The hardware-software co-design principle emerges clearly: algorithmic techniques like mixed precision unlock specialized hardware capabilities, while hardware features like Tensor Cores make certain algorithms practical. ### Flash Attention: IO-Aware Attention Optimization {#sec-model-training-flash-attention-ioaware-attention-optimization-3da0} \index{Flash Attention!IO-aware algorithm}\index{Flash Attention!memory bandwidth optimization}Mixed-precision training addresses two bottlenecks: compute throughput (Tensor Cores operate faster on FP16) and memory capacity (half the bytes per value). But for transformer models during training, a third bottleneck often dominates: *memory bandwidth*. The attention mechanism's quadratic intermediate matrices must be repeatedly loaded and stored during the forward pass and accessed again during backpropagation. Even with reduced precision, the sheer volume of memory traffic can leave compute units idle—GPUs waiting for data rather than computing. -Flash Attention [@dao2022flashattention] addresses this bandwidth bottleneck through a radically different approach: rather than optimizing *what precision* to use, it optimizes *how data flows* between memory hierarchies. By processing attention in small tiles that fit in fast on-chip SRAM, Flash Attention avoids materializing the full $n \times n$ attention matrix in slow HBM. This algorithmic restructuring achieves 2--4$\times$ training speedups while enabling training on sequences that would otherwise cause out-of-memory errors. +Flash Attention [@dao2022flashattention] addresses this bandwidth bottleneck through a radically different approach: rather than optimizing *what precision* to use, it optimizes *how data flows* between memory hierarchies. By processing attention in small tiles that fit in fast on-chip SRAM, Flash Attention avoids materializing the full $n \times n$ attention matrix in slow HBM. This algorithmic restructuring achieves 2--4 $\times$ training speedups while enabling training on sequences that would otherwise cause out-of-memory errors. #### The Standard Attention Memory Bottleneck {#sec-model-training-standard-attention-memory-bottleneck-6f39} @@ -3568,7 +3563,7 @@ total_attn_gb_str = fmt(total_attn_gb, precision=0, commas=False) The memory bottleneck emerges from materializing the n $\times$ n intermediate matrices for scores and probabilities. For a sequence length of `{python} fa_seq_len_str` tokens with embedding dimension `{python} embed_dim_str` (typical for a single attention head), the attention score matrix alone requires `{python} fa_seq_len_str`^2 $\times$ `{python} ResNetMemoryScaling.bytes_fp32_str` bytes = `{python} attn_matrix_mb_str` MB in FP32. With `{python} fa_n_heads_str` attention heads, this grows to `{python} total_attn_gb_str` GB just for intermediate attention matrices, not including the keys, queries, values, or output tensors. -\index{HBM!memory bandwidth}\index{SRAM!on-chip memory}Modern GPU memory hierarchy exacerbates this bottleneck. HBM provides 40–80 GB capacity with 1–2 TB/s bandwidth, while SRAM provides only 20–40 MB capacity but delivers 20+ TB/s bandwidth (10$\times$ faster). Standard attention stores these large matrices in slow HBM and repeatedly loads them during the backward pass. For GPT-2 scale models processing 2048-token sequences, attention operations spend 70--80% of execution time waiting for memory transfers rather than computing, leaving expensive tensor cores underutilized. +\index{HBM!memory bandwidth}\index{SRAM!on-chip memory}Modern GPU memory hierarchy exacerbates this bottleneck. HBM provides 40–80 GB capacity with 1–2 TB/s bandwidth, while SRAM provides only 20–40 MB capacity but delivers 20+ TB/s bandwidth (10 $\times$ faster). Standard attention stores these large matrices in slow HBM and repeatedly loads them during the backward pass. For GPT-2 scale models processing 2048-token sequences, attention operations spend 70--80% of execution time waiting for memory transfers rather than computing, leaving expensive tensor cores underutilized. The backward pass compounds this problem. Computing $\frac{\partial \mathcal{L}}{\partial Q}$, $\frac{\partial \mathcal{L}}{\partial K}$, and $\frac{\partial \mathcal{L}}{\partial V}$ requires access to the attention probability matrix $P = \text{softmax}(S)$ and the raw scores $S = QK^T/\sqrt{d_k}$ from the forward pass. Differentiating through softmax couples every element of $P$ to every element of $S$, so the full $n \times n$ matrices must be stored: @@ -3654,7 +3649,7 @@ fa_reduction_str = fmt(fa_reduction, precision=0, commas=False) ``` \index{Flash Attention!IO complexity reduction} -For n = 4096, d = 64: Standard attention requires 4096^2 $\times$ 4 bytes = `{python} fa_standard_mb_str` MB per head. Flash Attention requires only (3 $\times$ 4096 $\times$ 64) $\times$ 4 bytes ≈ `{python} fa_flash_mb_str` MB per head, a **`{python} fa_reduction_str`$\times$ reduction**. +For n = 4096, d = 64: Standard attention requires 4096^2 $\times$ 4 bytes = `{python} fa_standard_mb_str` MB per head. Flash Attention requires only (3 $\times$ 4096 $\times$ 64) $\times$ 4 bytes ≈ `{python} fa_flash_mb_str` MB per head, a **`{python} fa_reduction_str` $\times$ reduction**. ##### IO Complexity (Memory Reads/Writes) {#sec-model-training-io-complexity-memory-readswrites-4af5} @@ -3666,7 +3661,7 @@ Standard attention performs: Flash Attention performs different memory operations. In the forward pass, it reads $Q, K, V$ once and writes $O$ once, requiring $O(n \cdot d)$ bytes. In the backward pass, it recomputes $S, P$ in SRAM from $Q, K, V$ and writes $dQ, dK, dV$, again requiring $O(n \cdot d)$ bytes. Total HBM accesses are $O(n \cdot d)$. -For large sequence lengths where $n \gg d$, Flash Attention reduces memory traffic by a factor of $n$. With $n = 4096$ and $d = 64$, this represents a **64$\times$ reduction** in memory bandwidth consumption. +For large sequence lengths where $n \gg d$, Flash Attention reduces memory traffic by a factor of $n$. With $n = 4096$ and $d = 64$, this represents a **64 $\times$ reduction** in memory bandwidth consumption. ##### Computational Complexity {#sec-model-training-computational-complexity-7d3b} @@ -3676,24 +3671,24 @@ Both approaches require $O(n^2 d)$ FLOPs for attention computation. Flash Attent Flash Attention's performance gains materialize through careful exploitation of GPU memory hierarchy. Modern frameworks integrate these optimizations transparently, automatically selecting the most efficient attention implementation based on hardware capabilities and input characteristics. @lst-flash-attention-comparison contrasts standard and optimized attention implementations. -::: {#lst-flash-attention-comparison lst-cap="**Attention Implementation Comparison**: Standard attention materializes the full n$\times$n matrix in HBM, while Flash Attention uses PyTorch's optimized implementation or the dedicated flash-attn library."} +::: {#lst-flash-attention-comparison lst-cap="**Attention Implementation Comparison**: Standard attention materializes the full n $\times$ n matrix in HBM, while Flash Attention uses PyTorch's optimized implementation or the dedicated flash-attn library."} ```{.python} import torch import torch.nn.functional as F -# Standard attention (materializes n×n matrix) +# Standard attention (materializes n $\times$ n matrix) def standard_attention(q, k, v): # q, k, v: [batch, heads, seq_len, head_dim] scores = torch.matmul(q, k.transpose(-2, -1)) / ( q.size(-1) ** 0.5 ) - attn = F.softmax(scores, dim=-1) # n×n matrix in HBM + attn = F.softmax(scores, dim=-1) # n $\times$ n matrix in HBM output = torch.matmul(attn, v) return output -# Flash Attention (no n×n materialization) +# Flash Attention (no n $\times$ n materialization) def flash_attention(q, k, v): # Automatically uses Flash Attention if available output = F.scaled_dot_product_attention(q, k, v) @@ -3771,10 +3766,10 @@ flash_fwd_speedup_str = FlashAttentionSpeedup.flash_fwd_speedup_str flash_bwd_speedup_str = FlashAttentionSpeedup.flash_bwd_speedup_str ``` -Standard attention runs out of memory beyond 2048 tokens on a 40 GB A100, while Flash Attention trains sequences up to 8192 tokens. Even at 2048 tokens where both fit, Flash Attention achieves `{python} flash_fwd_speedup_str`$\times$ forward pass speedup and `{python} flash_bwd_speedup_str`$\times$ backward pass speedup. +Standard attention runs out of memory beyond 2048 tokens on a 40 GB A100, while Flash Attention trains sequences up to 8192 tokens. Even at 2048 tokens where both fit, Flash Attention achieves `{python} flash_fwd_speedup_str` $\times$ forward pass speedup and `{python} flash_bwd_speedup_str` $\times$ backward pass speedup. \index{Flash Attention 2!improved parallelism}\index{Flash Attention 3!FP8 tensor cores} -Subsequent versions have continued improving performance: Flash Attention 2 (2023) achieved 1.5--2$\times$ additional speedup through better parallelism and register allocation, while Flash Attention 3 (2024) exploits FP8 tensor cores and asynchronous memory operations on Hopper GPUs to reach 740 TFLOPS on H100 (75% of theoretical peak). +Subsequent versions have continued improving performance: Flash Attention 2 (2023) achieved 1.5--2 $\times$ additional speedup through better parallelism and register allocation, while Flash Attention 3 (2024) exploits FP8 tensor cores and asynchronous memory operations on Hopper GPUs to reach 740 TFLOPS on H100 (75% of theoretical peak). #### When to Use Flash Attention {#sec-model-training-use-flash-attention-375d} @@ -3795,7 +3790,7 @@ In practice, deep learning frameworks handle Flash Attention integration transpa 1. Ensure tensor layouts match library expectations (contiguous memory, correct dimension ordering) 2. Use FP16 or BF16 for maximum speedup (Flash Attention optimized for mixed precision) -3. Combine with gradient checkpointing for further memory savings (4--8$\times$ larger models trainable) +3. Combine with gradient checkpointing for further memory savings (4--8 $\times$ larger models trainable) The integration is typically a single-line change---swapping a manual attention call for `F.scaled_dot_product_attention` (as shown in @lst-flash-attention-comparison). By using the framework's optimized primitive, the developer delegates the complex tiling and SRAM management required to bypass the HBM bandwidth bottleneck to the underlying library. @@ -3810,12 +3805,12 @@ This principle extends beyond attention: **Communication-efficient distributed training.** Gradient compression techniques apply similar principles, trading extra computation (compression/decompression) for reduced network bandwidth consumption. -**Edge deployment.** Low-power edge devices with limited memory bandwidth benefit even more from IO-aware algorithms, where a 10% increase in FLOPs that halves memory traffic yields 3--5$\times$ energy savings. +**Edge deployment.** Low-power edge devices with limited memory bandwidth benefit even more from IO-aware algorithms, where a 10% increase in FLOPs that halves memory traffic yields 3--5 $\times$ energy savings. Flash Attention's impact on practical model training capabilities is substantial. By eliminating the $O(n^2)$ memory bottleneck, it enables: -- **4$\times$ longer sequences** on the same hardware (2K → 8K context for GPT-2 on A100) -- **2$\times$ larger batch sizes** through freed memory (faster convergence) +- **4 $\times$ longer sequences** on the same hardware (2K → 8K context for GPT-2 on A100) +- **2 $\times$ larger batch sizes** through freed memory (faster convergence) - **Deeper models** by reducing activation memory (more layers fit in same budget) For a 7B parameter model training on A100 GPUs, Flash Attention transforms training from infeasible (OOM at 2K context) to practical (8K context with room for batch size 32), representing the difference between a model that cannot be trained and one deployed in production. @@ -3930,7 +3925,7 @@ $$ T_{\text{effective}} = k \times T_{\text{micro}} + (k-1) \times T_{\text{sync}} $$ {#eq-gradient-accumulation-overhead} -In practice, this overhead is small compared to the memory savings. Training BERT-Large with effective batch size 256 using 8 accumulation steps of micro-batch 32 reduces activation memory by 8$\times$ while adding only 10--15% to wall-clock time. +In practice, this overhead is small compared to the memory savings. Training BERT-Large with effective batch size 256 using 8 accumulation steps of micro-batch 32 reduces activation memory by 8 $\times$ while adding only 10--15% to wall-clock time. When gradient accumulation is combined with distributed data parallelism across multiple machines, additional considerations arise for gradient synchronization timing and effective batch size calculation across the cluster. These distributed training patterns are explored in advanced distributed systems texts. @@ -4023,9 +4018,9 @@ Not all operations are equally expensive to recompute, which motivates *selectiv Gradient accumulation[^fn-gradient-accumulation-training] simulates larger batch sizes without increasing memory requirements for storing the full batch. Larger batch sizes improve gradient estimates, leading to more stable convergence and faster training. This flexibility proves particularly valuable when training on high-resolution data where even a single batch may exceed available memory. -[^fn-gradient-accumulation-training]: **Gradient Accumulation Impact**: Enables effective batch sizes of 2048+ on single GPUs with only 32--64 micro-batch size, essential for transformer training. BERT-Large training uses effective batch size of 256 (accumulated over 8 steps) achieving 99.5% of full-batch performance while reducing memory requirements by 8$\times$. The technique trades 10--15% compute overhead for massive memory savings. +[^fn-gradient-accumulation-training]: **Gradient Accumulation Impact**: Enables effective batch sizes of 2048+ on single GPUs with only 32--64 micro-batch size, essential for transformer training. BERT-Large training uses effective batch size of 256 (accumulated over 8 steps) achieving 99.5% of full-batch performance while reducing memory requirements by 8 $\times$. The technique trades 10--15% compute overhead for massive memory savings. -[^fn-training-activation-checkpointing]: **Activation Checkpointing Trade-offs**: Reduces memory usage by 50--90% at the cost of 15--30% additional compute time due to recomputation. For training GPT-3 on V100s, checkpointing enables 2.8$\times$ larger models (from 1.3 B to 3.7 B parameters) within `{python} TrainingHardware.v100_mem_str` GB memory constraints, making it essential for memory-bound large model training despite the compute penalty. +[^fn-training-activation-checkpointing]: **Activation Checkpointing Trade-offs**: Reduces memory usage by 50--90% at the cost of 15--30% additional compute time due to recomputation. For training GPT-3 on V100s, checkpointing enables 2.8 $\times$ larger models (from 1.3 B to 3.7 B parameters) within `{python} TrainingHardware.v100_mem_str` GB memory constraints, making it essential for memory-bound large model training despite the compute penalty. Activation checkpointing[^fn-training-activation-checkpointing] significantly reduces the memory footprint of intermediate activations during the forward pass, allowing training of deeper models. By discarding and recomputing activations as needed, checkpointing frees up memory for larger models, additional layers, or higher resolution data. This is especially important in advanced architectures like transformers that require substantial memory for intermediate computations. @@ -4156,9 +4151,9 @@ With Accumulation (actual GPT-2 approach): **Tradeoff Analysis** -- Compute overhead: 4$\times$ forward passes per update = ~8% slower (pipeline overlaps some cost) +- Compute overhead: 4 $\times$ forward passes per update = ~8% slower (pipeline overlaps some cost) - Memory overhead: Gradient accumulation buffer = negligible (gradients already needed) -- Communication benefit: Sync frequency reduced by `{python} accum_steps_str`$\times$ → communication time drops by `{python} comm_reduction_pct_str`% +- Communication benefit: Sync frequency reduced by `{python} accum_steps_str` $\times$ → communication time drops by `{python} comm_reduction_pct_str`% - Cost benefit: Training 2 weeks on 8 GPUs = USD `{python} accum_2wk_str` K vs. 32 GPUs = USD `{python} naive_2wk_str` K **Convergence Quality** @@ -4199,7 +4194,7 @@ Both techniques introduce explicit trade-offs. Activation checkpointing adds app ### Optimization Technique Comparison {#sec-model-training-optimization-technique-comparison-a89a} -@tbl-optimization synthesizes three of the four core optimization strategies, contrasting their primary goals, mechanisms, and trade-offs. Flash Attention (@sec-model-training-flash-attention-ioaware-attention-optimization-3da0) complements these by addressing memory-bandwidth bottlenecks in attention layers through IO-aware tiling, achieving 2–4$\times$ speedups while reducing memory from $O(n^2)$ to $O(n)$. Selecting an appropriate strategy depends on the specific bottleneck identified through profiling. +@tbl-optimization synthesizes three of the four core optimization strategies, contrasting their primary goals, mechanisms, and trade-offs. Flash Attention (@sec-model-training-flash-attention-ioaware-attention-optimization-3da0) complements these by addressing memory-bandwidth bottlenecks in attention layers through IO-aware tiling, achieving 2–4 $\times$ speedups while reducing memory from $O(n^2)$ to $O(n)$. Selecting an appropriate strategy depends on the specific bottleneck identified through profiling. | **Aspect** | **Prefetching and Overlapping** | **Mixed-Precision Training** | **Gradient Accumulation and Checkpointing** | |:------------------------------|:-----------------------------------------------------------|:----------------------------------------------------------|:-------------------------------------------------------------------------| @@ -4317,7 +4312,7 @@ checkpoint_factor_str = f"{checkpoint_factor}" #### Steps 1--3: Solve the Memory Problem {.unnumbered} -The memory analysis from @sec-model-training-mixedprecision-training-9218 applies directly. Baseline FP32 requires `{python} total_fp32_str` GB---immediate OOM on a 32 GB V100 (**Machine** constraint in D·A·M terms). Applying mixed precision (AMP) reduces this to `{python} total_amp_str` GB (`{python} amp_reduction_str`% reduction), but still exceeds 32 GB. Adding gradient checkpointing (every 4 layers) reduces activations by 4$\times$, bringing the total to `{python} total_ckpt_str` GB---it fits, at the cost of `{python} recompute_overhead_str`% more compute for activation recomputation. +The memory analysis from @sec-model-training-mixedprecision-training-9218 applies directly. Baseline FP32 requires `{python} total_fp32_str` GB---immediate OOM on a 32 GB V100 (**Machine** constraint in D·A·M terms). Applying mixed precision (AMP) reduces this to `{python} total_amp_str` GB (`{python} amp_reduction_str`% reduction), but still exceeds 32 GB. Adding gradient checkpointing (every 4 layers) reduces activations by 4 $\times$, bringing the total to `{python} total_ckpt_str` GB---it fits, at the cost of `{python} recompute_overhead_str`% more compute for activation recomputation. With memory solved, the interesting question is: *is throughput acceptable?* @@ -4347,12 +4342,12 @@ After optimization: #### Step 6: Final Profile and Results {.unnumbered} -| **Metric** | **Naive** | **Optimized** | **Improvement** | -|:--------------------|:----------|-----------------:|:----------------------| -| **Memory** | 89 GB | 32 GB | 2.8$\times$ reduction | -| **GPU utilization** | N/A | 85% | Trainable | -| **Throughput** | N/A | 1,200 tokens/sec | — | -| **Time per epoch** | N/A | 8.3 hours | — | +| **Metric** | **Naive** | **Optimized** | **Improvement** | +|:--------------------|:----------|-----------------:|:-----------------------| +| **Memory** | 89 GB | 32 GB | 2.8 $\times$ reduction | +| **GPU utilization** | N/A | 85% | Trainable | +| **Throughput** | N/A | 1,200 tokens/sec | — | +| **Time per epoch** | N/A | 8.3 hours | — | Remaining bottleneck: compute-bound---an *Algorithm* constraint in D·A·M terms (as desired). The 85% utilization indicates good efficiency; remaining 15% is overhead from gradient synchronization, loss scaling, and kernel launch latency. @@ -4362,7 +4357,7 @@ Three key principles emerge from this analysis: 1. **Profile before optimizing**: Each optimization targeted a specific bottleneck revealed by profiling 2. **Techniques compose**: Mixed precision alone wasn't enough; combining it with checkpointing and prefetching achieved the goal -3. **Trade-offs are explicit**: We accepted `{python} recompute_overhead_str`% more compute (checkpointing) to gain ~3$\times$ memory reduction +3. **Trade-offs are explicit**: We accepted `{python} recompute_overhead_str`% more compute (checkpointing) to gain ~3 $\times$ memory reduction The systematic framework—profile, identify bottleneck, apply targeted technique, re-profile—transforms optimization from trial-and-error into engineering practice. @@ -4452,7 +4447,7 @@ o_carbon_str = f"~{fmt(o_carbon, precision=0, commas=False)} tons CO₂" | **Optimizer State (Adam)** | `{python} b_opt_str` | `{python} o_opt_str` | Unchanged (FP32 moments) | | **Activations (batch=32)** | `{python} b_act_str` | `{python} o_act_str` | Gradient checkpointing + FP16 | | **Total Memory** | **`{python} b_total_mem_str`** | **`{python} o_total_mem_str`** | — | -| **Training Time (32 V100s)** | `{python} b_time_str` | `{python} o_time_str` | 2.4$\times$ Tensor Core speedup | +| **Training Time (32 V100s)** | `{python} b_time_str` | `{python} o_time_str` | 2.4 $\times$ Tensor Core speedup | | **Energy Consumption** | `{python} b_energy_str` | `{python} o_energy_str` | Reduced time + improved efficiency | | **Electricity Cost (USD 0.10/kWh)** | `{python} b_cost_str` | `{python} o_cost_str` | — | | **Carbon Footprint** | `{python} b_carbon_str` | `{python} o_carbon_str` | Regional grid average (0.45 kg/kWh) | @@ -4496,7 +4491,7 @@ energy_reduction_pct_str = fmt(energy_reduction_pct, precision=0, commas=False) time_speedup_str = fmt(time_speedup, precision=1, commas=False) ``` -As @tbl-gpt2-summary shows, this `{python} mem_reduction_str`$\times$ memory reduction, combined with `{python} time_speedup_str`$\times$ computational speedup and `{python} energy_reduction_pct_str`% energy reduction, exemplifies how systematic optimization transforms hardware constraints into engineering design parameters. The same optimizations that improve throughput also reduce energy consumption and operational cost. +As @tbl-gpt2-summary shows, this `{python} mem_reduction_str` $\times$ memory reduction, combined with `{python} time_speedup_str` $\times$ computational speedup and `{python} energy_reduction_pct_str`% energy reduction, exemplifies how systematic optimization transforms hardware constraints into engineering design parameters. The same optimizations that improve throughput also reduce energy consumption and operational cost. The single-machine optimization toolkit is now exhausted. Mixed precision extracts maximum throughput from Tensor Cores. Flash Attention reduces bandwidth consumption to near-theoretical minimums. Gradient checkpointing trades compute for memory at favorable ratios. Prefetching hides data loading latency. A well-optimized single GPU can train GPT-2 scale models in days rather than weeks. @@ -4697,7 +4692,7 @@ inner ysep=18, fill=BackColor,yshift=6, fit=(B4)(G4)](BB4){}; Model parallelism's challenge is *idle time*. While Device 1 computes layers 1--6, Devices 2--4 sit idle waiting for activations. During the backward pass, the problem reverses: Device 4 computes first while others wait. This "pipeline bubble" means naive model parallelism achieves poor GPU utilization---often 25--50% even with careful partitioning. We'll see how pipeline parallelism addresses this inefficiency when we discuss distributed strategies. \index{NVLink!GPU interconnect}\index{NVLink!gradient synchronization} -Within a single node, GPUs communicate via high-bandwidth interconnects like NVLink[^fn-nvlink-training] (up to `{python} TrainingHardware.nvlink_h100_str` GB/s on modern systems), making gradient synchronization and activation transfers fast. Data parallelism transfers gradients, which are proportional to model size. Model parallelism transfers activations, proportional to batch size times hidden dimension, at every partition boundary. The 10--50$\times$ bandwidth advantage of NVLink over PCIe makes both strategies practical within a node. This intra-node parallelism forms the building block for larger distributed systems. To understand *when* to choose each strategy, the following analysis compares *data vs. model parallelism* quantitatively. +Within a single node, GPUs communicate via high-bandwidth interconnects like NVLink[^fn-nvlink-training] (up to `{python} TrainingHardware.nvlink_h100_str` GB/s on modern systems), making gradient synchronization and activation transfers fast. Data parallelism transfers gradients, which are proportional to model size. Model parallelism transfers activations, proportional to batch size times hidden dimension, at every partition boundary. The 10--50 $\times$ bandwidth advantage of NVLink over PCIe makes both strategies practical within a node. This intra-node parallelism forms the building block for larger distributed systems. To understand *when* to choose each strategy, the following analysis compares *data vs. model parallelism* quantitatively. ::: {.callout-notebook title="Data vs. Model Parallelism"} @@ -4725,7 +4720,7 @@ Within a single node, GPUs communicate via high-bandwidth interconnects like NVL * Use **Model Parallel** when the model is too big to fit in a single GPU's memory. ::: -[^fn-nvlink-training]: **NVLink**: NVIDIA's high-bandwidth GPU interconnect, providing 50--`{python} TrainingHardware.nvlink_h100_str` GB/s bidirectional bandwidth (depending on generation) compared to 16--64 GB/s for PCIe. For training, this 10–50$\times$ bandwidth advantage enables efficient gradient synchronization and model parallelism within a node. See @sec-ml-frameworks for additional details. +[^fn-nvlink-training]: **NVLink**: NVIDIA's high-bandwidth GPU interconnect, providing 50--`{python} TrainingHardware.nvlink_h100_str` GB/s bidirectional bandwidth (depending on generation) compared to 16--64 GB/s for PCIe. For training, this 10–50 $\times$ bandwidth advantage enables efficient gradient synchronization and model parallelism within a node. See @sec-ml-frameworks for additional details. ### Scaling Beyond a Single Node {#sec-model-training-scaling-beyond-single-node-a671} @@ -4734,7 +4729,7 @@ When single-node multi-GPU training remains insufficient, distributed training e ::: {.callout-perspective title="The Physics of Synchronization"} -Recall the **Energy-Movement Invariant** from @sec-data-engineering: moving data is 100–1,000$\times$ more expensive than computing on it. In distributed training, this physical law manifests as the **Communication Tax**. +Recall the **Energy-Movement Invariant** from @sec-data-engineering: moving data is 100–1,000 $\times$ more expensive than computing on it. In distributed training, this physical law manifests as the **Communication Tax**. When you synchronize gradients across a fleet of GPUs, you are moving megabytes of data across a network or PCIe bus for every few milliseconds of computation. If the energy required for communication ($E_{net}$) exceeds the energy for computation ($E_{compute}$), your system efficiency ($\eta$) collapses. This is why techniques like **Mixed Precision** (@sec-model-training-mixedprecision-training-9218) and **Gradient Compression** are essential: they aren't just "speedups"; they are essential tools for managing the physical limits of distributed scaling. ::: @@ -4759,7 +4754,7 @@ Hybrid strategies combine these approaches because each has different scaling ch The implementation details—gradient synchronization algorithms (AllReduce\index{AllReduce!gradient synchronization}\index{Ring AllReduce!bandwidth optimization}[^fn-allreduce], ring-reduce), communication patterns (parameter server, peer-to-peer), fault tolerance mechanisms, and scaling efficiency analysis for training runs spanning thousands of GPUs—constitute a specialized domain that builds on the foundations established here. -[^fn-allreduce]: **AllReduce**: A collective communication primitive that aggregates data across all participating devices and distributes the result back to each. For gradient synchronization, AllReduce sums gradients from all GPUs so each has the identical averaged gradient. Ring AllReduce [@patarasuk2009bandwidth], popularized by Baidu in 2017, achieves bandwidth-optimal performance by passing data in a ring topology, requiring only 2(N-1)/N of the data volume (approaching 2$\times$ for large N) regardless of participant count, making it the standard for data-parallel training. +[^fn-allreduce]: **AllReduce**: A collective communication primitive that aggregates data across all participating devices and distributes the result back to each. For gradient synchronization, AllReduce sums gradients from all GPUs so each has the identical averaged gradient. Ring AllReduce [@patarasuk2009bandwidth], popularized by Baidu in 2017, achieves bandwidth-optimal performance by passing data in a ring topology, requiring only 2(N-1)/N of the data volume (approaching 2 $\times$ for large N) regardless of participant count, making it the standard for data-parallel training. ### The Evolution of Training Infrastructure {#sec-model-training-evolution-training-infrastructure-f3a6} @@ -5119,7 +5114,7 @@ The allure of scale is powerful: if a 7B model works well, surely a 20B model wo **Pitfall:** *Assuming distributed training automatically accelerates development.* -More GPUs should mean faster training---but the communication tax (@sec-model-training-scaling-training-systems-adfd) often eats the gains. Small models on `{python} fp_gpu_count_str` GPUs spend `{python} fp_sync_overhead_range_str`% of time synchronizing gradients, achieving only `{python} fp_actual_speedup_range_str`$\times$ speedup instead of `{python} fp_gpu_count_str`$\times$. A well-optimized single A100 completing training in `{python} fp_single_gpu_hours_str` hours can outperform a poorly configured `{python} fp_gpu_count_str`-GPU cluster taking `{python} fp_cluster_hours_str` hours. The overhead of debugging distributed configurations, managing gradient synchronization, and handling stragglers often exceeds the time saved. Always profile and exhaust single-machine optimizations before distributing. +More GPUs should mean faster training---but the communication tax (@sec-model-training-scaling-training-systems-adfd) often eats the gains. Small models on `{python} fp_gpu_count_str` GPUs spend `{python} fp_sync_overhead_range_str`% of time synchronizing gradients, achieving only `{python} fp_actual_speedup_range_str` $\times$ speedup instead of `{python} fp_gpu_count_str` $\times$. A well-optimized single A100 completing training in `{python} fp_single_gpu_hours_str` hours can outperform a poorly configured `{python} fp_gpu_count_str`-GPU cluster taking `{python} fp_cluster_hours_str` hours. The overhead of debugging distributed configurations, managing gradient synchronization, and handling stragglers often exceeds the time saved. Always profile and exhaust single-machine optimizations before distributing. **Fallacy:** *Hyperparameters transfer directly from small-scale experiments to large-scale training.* @@ -5127,11 +5122,11 @@ A learning rate that works at batch size `{python} fp_batch_small_str` does not **Pitfall:** *Treating mixed precision training as a simple toggle without validation.* -Mixed precision achieves `{python} fp_mp_speedup_v100_str`$\times$ speedup on V100 Tensor Cores but requires loss scaling to prevent gradient underflow (see @sec-model-training-mixedprecision-training-9218). A language model training for `{python} fp_training_hours_str` hours can diverge at step `{python} fp_divergence_step_str` due to accumulated numerical errors. Always validate mixed precision convergence on representative workloads before deploying at scale. +Mixed precision achieves `{python} fp_mp_speedup_v100_str` $\times$ speedup on V100 Tensor Cores but requires loss scaling to prevent gradient underflow (see @sec-model-training-mixedprecision-training-9218). A language model training for `{python} fp_training_hours_str` hours can diverge at step `{python} fp_divergence_step_str` due to accumulated numerical errors. Always validate mixed precision convergence on representative workloads before deploying at scale. **Pitfall:** *Optimizing memory and computation independently.* -Memory and compute are coupled: GPU utilization drops from `{python} fp_util_batch_256_str` percent at batch 256 to `{python} fp_util_batch_16_range_str` percent at batch 16. Gradient accumulation (effective batch `{python} fp_effective_batch_str`, physical batch `{python} fp_physical_batch_str`) trades `{python} fp_util_diff_str` percent efficiency for `{python} fp_memory_reduction_str`$\times$ memory reduction. Tuning these parameters independently extends training time by `{python} fp_time_extension_range_str` percent (see @sec-model-training-gradient-accumulation-checkpointing-0c47). +Memory and compute are coupled: GPU utilization drops from `{python} fp_util_batch_256_str` percent at batch 256 to `{python} fp_util_batch_16_range_str` percent at batch 16. Gradient accumulation (effective batch `{python} fp_effective_batch_str`, physical batch `{python} fp_physical_batch_str`) trades `{python} fp_util_diff_str` percent efficiency for `{python} fp_memory_reduction_str` $\times$ memory reduction. Tuning these parameters independently extends training time by `{python} fp_time_extension_range_str` percent (see @sec-model-training-gradient-accumulation-checkpointing-0c47). **Pitfall:** *Neglecting data pipeline optimization until GPU utilization profiling.* @@ -5148,12 +5143,12 @@ This co-design principle---where algorithms, software frameworks, and hardware a ::: {.callout-takeaways title="Why Training Costs Millions"} * **The Iron Law governs training**: $T_{train} = \frac{O}{R_{peak} \times \eta}$. Every optimization affects one of these terms. Identifying which term is affected is essential for effective optimization. -* **Memory is dominated by optimizer state and activations, not weights**: Adam's two state vectors per parameter create a 3$\times$ multiplier over model size, and activation memory scales linearly with batch size and depth. Together, these determine whether a model fits on a given GPU---not the parameter count alone. -* **Optimizer selection is a memory-convergence tradeoff**: Adam converges in roughly one-third the iterations of SGD but requires 3$\times$ the memory for per-parameter state, making the choice a binding constraint for large model training. Variants like AdamW and 8-bit Adam shift this tradeoff without eliminating it. +* **Memory is dominated by optimizer state and activations, not weights**: Adam's two state vectors per parameter create a 3 $\times$ multiplier over model size, and activation memory scales linearly with batch size and depth. Together, these determine whether a model fits on a given GPU---not the parameter count alone. +* **Optimizer selection is a memory-convergence tradeoff**: Adam converges in roughly one-third the iterations of SGD but requires 3 $\times$ the memory for per-parameter state, making the choice a binding constraint for large model training. Variants like AdamW and 8-bit Adam shift this tradeoff without eliminating it. * **Profiling precedes optimization**: The iterative loop is: profile → identify bottleneck → apply targeted fix → re-profile. Optimization without profiling typically wastes effort on non-bottlenecks. -* **Mixed precision provides substantial performance gains**: FP16 training with FP32 accumulation delivers approximately 2$\times$ throughput and 2$\times$ memory reduction with typically <1% accuracy impact on most workloads. -* **IO-aware algorithm design transforms bottleneck regimes**: Flash Attention's tiling strategy converts attention from memory-bound to compute-bound by never materializing the full $n \times n$ matrix in HBM, achieving 2--4$\times$ speedups and enabling 4$\times$ longer sequences. The principle extends beyond attention: optimizing data movement often yields larger gains than optimizing computation. -* **Gradient checkpointing trades compute for memory**: Recomputing activations during the backward pass enables training larger models (e.g., GPT-3 scales from 1.3B to 3.7B parameters on V100s) or achieves 3–4$\times$ activation memory reduction. Essential when memory is the binding constraint. +* **Mixed precision provides substantial performance gains**: FP16 training with FP32 accumulation delivers approximately 2 $\times$ throughput and 2 $\times$ memory reduction with typically <1% accuracy impact on most workloads. +* **IO-aware algorithm design transforms bottleneck regimes**: Flash Attention's tiling strategy converts attention from memory-bound to compute-bound by never materializing the full $n \times n$ matrix in HBM, achieving 2--4 $\times$ speedups and enabling 4 $\times$ longer sequences. The principle extends beyond attention: optimizing data movement often yields larger gains than optimizing computation. +* **Gradient checkpointing trades compute for memory**: Recomputing activations during the backward pass enables training larger models (e.g., GPT-3 scales from 1.3B to 3.7B parameters on V100s) or achieves 3–4 $\times$ activation memory reduction. Essential when memory is the binding constraint. * **Optimizations compose---and must be applied systematically**: No single technique addresses all bottlenecks. The GPT-2 walkthrough demonstrates how mixed precision, gradient checkpointing, and data prefetching must be layered together, guided by iterative profiling, to transform an infeasible 89 GB memory requirement into a practical 32 GB configuration. * **Single-machine optimizations reduce the cost of scaling out**: Distributed training adds communication overhead and complexity. A well-optimized single GPU often outperforms a poorly-optimized multi-GPU setup; assess prefetching, mixed precision, checkpointing, and accumulation before scaling out. * **Energy and cost scale linearly with training time**: The same optimizations that accelerate training also reduce carbon emissions and cloud costs. Efficiency improvements directly translate to reduced resource consumption. @@ -5164,7 +5159,7 @@ The practitioners who internalize the **Iron Law** can look at a slow training j ::: {.callout-chapter-connection title="From Training to Compression"} -Training produces the model artifact---a collection of billions of learned parameters that encode patterns extracted from data. The expense of creating this artifact is incurred once, but the real challenge begins when it must be *deployed*---on cloud servers processing thousands of requests per second, on edge devices with limited memory, or on mobile phones with strict power budgets. A model that required 32 GB to train is far too large to serve efficiently in these environments. The next chapter, @sec-model-compression, begins the optimization journey by asking: how much of this trained model is actually necessary? Quantization reduces parameter precision from 32-bit floats to 4-bit integers, shrinking the model by 8$\times$ with minimal accuracy loss; pruning removes redundant connections entirely; knowledge distillation compresses a large "teacher" model into a smaller "student." These compression techniques transform training's output---an expensively trained but unwieldy model---into something deployable in the real world. +Training produces the model artifact---a collection of billions of learned parameters that encode patterns extracted from data. The expense of creating this artifact is incurred once, but the real challenge begins when it must be *deployed*---on cloud servers processing thousands of requests per second, on edge devices with limited memory, or on mobile phones with strict power budgets. A model that required 32 GB to train is far too large to serve efficiently in these environments. The next chapter, @sec-model-compression, begins the optimization journey by asking: how much of this trained model is actually necessary? Quantization reduces parameter precision from 32-bit floats to 4-bit integers, shrinking the model by 8 $\times$ with minimal accuracy loss; pruning removes redundant connections entirely; knowledge distillation compresses a large "teacher" model into a smaller "student." These compression techniques transform training's output---an expensively trained but unwieldy model---into something deployable in the real world. ::: diff --git a/book/quarto/contents/vol2/ai_good/ai_good.qmd b/book/quarto/contents/vol2/ai_good/ai_good.qmd index 5cb4e01efc..2f7a999078 100644 --- a/book/quarto/contents/vol2/ai_good/ai_good.qmd +++ b/book/quarto/contents/vol2/ai_good/ai_good.qmd @@ -80,7 +80,7 @@ This broader scientific transformation represents a shift to the "Fourth Paradig In biology, AlphaFold solved the 50-year-old protein folding problem, predicting 3D structures for nearly all known proteins. This accelerates drug discovery and disease understanding by orders of magnitude. In physics, deep reinforcement learning controls nuclear fusion plasma in tokamaks, maintaining stability in regimes that traditional control theory cannot handle. In materials science, graph neural networks predict properties of millions of crystal structures, identifying candidates for better batteries and solar cells without expensive physical synthesis. -In these domains, AI acts not just as a pattern matcher but as a **simulation surrogate**, approximating complex physical equations 1000$\times$ faster than traditional solvers, enabling scientists to explore vast hypothesis spaces previously out of reach. +In these domains, AI acts not just as a pattern matcher but as a **simulation surrogate**, approximating complex physical equations 1000 $\times$ faster than traditional solvers, enabling scientists to explore vast hypothesis spaces previously out of reach. #### Bridging the Digital Language Divide {#sec-ai-good-bridging-digital-language-divide-f9b3} @@ -150,7 +150,7 @@ Disaster response demands rapid decision making under extreme uncertainty, often This capability proves vital in disaster zones, where AI technologies accelerate response efforts and enhance safety. Tiny autonomous drones equipped with TinyML algorithms are making their way into collapsed buildings, navigating obstacles to detect signs of life. By analyzing thermal imaging[^fn-thermal-imaging-rescue] and acoustic signals locally, these drones can identify survivors and hazards without relying on cloud connectivity [@duisterhof2021sniffy]. These drones can autonomously seek light sources (which often indicate survivors) and detect dangerous gas leaks, making search and rescue operations both faster and safer for human responders. -[^fn-thermal-imaging-rescue]: **Thermal Imaging in Disaster Response**: Human body temperature (37°C) contrasts sharply with debris temperature (often 15-25°C), enabling detection through 30cm of rubble. TinyML thermal analysis on drones can process 320$\times$240 pixel thermal images at 9Hz using only 500mW power, operating for 20+ minutes on small batteries. This autonomous capability proved critical during the 2023 Turkey earthquake, where 72-hour survival windows made rapid victim location essential for the 50,000+ people trapped. +[^fn-thermal-imaging-rescue]: **Thermal Imaging in Disaster Response**: Human body temperature (37°C) contrasts sharply with debris temperature (often 15-25°C), enabling detection through 30cm of rubble. TinyML thermal analysis on drones can process 320 $\times$ 240 pixel thermal images at 9Hz using only 500mW power, operating for 20+ minutes on small batteries. This autonomous capability proved critical during the 2023 Turkey earthquake, where 72-hour survival windows made rapid victim location essential for the 50,000+ people trapped. {{< margin-video "https://www.youtube.com/watch?v=hj_SBSpK5qg" "Gas Seeking" "TU Delft" >}} @@ -232,12 +232,12 @@ Achieving ultra-low model sizes for social good applications requires systematic [^fn-knowledge-distillation]: **Knowledge Distillation**: A model compression technique where a smaller "student" model learns to mimic the behavior of a larger "teacher" model, not just matching the teacher's predictions but learning its decision-making patterns through soft probability distributions. For social good deployments, distillation enables a 500KB student model to capture 85-95% of a 100MB teacher's accuracy. This is particularly valuable because the expensive teacher model can be trained once in the cloud, then distilled into multiple specialized student models for different edge devices and deployment contexts. -To illustrate these optimization requirements, the optimization pipeline for the PlantVillage crop disease detection system demonstrates quantitative compression trade-offs. Starting with a ResNet-50 architecture [@he2016deep] at 100 MB achieving 91% accuracy, systematic optimization reduces model size by 31$\times$ while maintaining practical effectiveness: +To illustrate these optimization requirements, the optimization pipeline for the PlantVillage crop disease detection system demonstrates quantitative compression trade-offs. Starting with a ResNet-50 architecture [@he2016deep] at 100 MB achieving 91% accuracy, systematic optimization reduces model size by 31 $\times$ while maintaining practical effectiveness: - **Original ResNet-50**: ~98 MB (FP32), 91% accuracy baseline on crop disease dataset -- **8-bit quantization**: 25 MB, 89% accuracy (4$\times$ compression, 2% accuracy loss) -- **Structured pruning**: 8 MB, 88% accuracy (12$\times$ compression, 3% accuracy loss) -- **Knowledge distillation**: 3.2 MB, 87% accuracy (31$\times$ compression, 4% accuracy loss) +- **8-bit quantization**: 25 MB, 89% accuracy (4 $\times$ compression, 2% accuracy loss) +- **Structured pruning**: 8 MB, 88% accuracy (12 $\times$ compression, 3% accuracy loss) +- **Knowledge distillation**: 3.2 MB, 87% accuracy (31 $\times$ compression, 4% accuracy loss) These compression ratios enable deployment on resource-constrained devices while preserving diagnostic capabilities essential for rural farmers. The final 3.2 MB model requires only 50-80 milliseconds for inference on an ESP32 microcontroller, enabling real-time crop disease detection in off-grid agricultural environments. @@ -352,11 +352,11 @@ These patterns provide systematic frameworks for navigating trade-offs between f Moving from data constraints to deployment realities, scaling machine learning systems from prototype to production deployment introduces core resource constraints that necessitate architectural redesign. Development environments provide computational resources that mask many real-world limitations. A typical development platform, such as a Raspberry Pi 4[^fn-raspberry-pi-development], offers substantial computing power with its 1.5 GHz processor and 4 GB RAM. These resources allow rapid prototyping and testing of machine learning models without immediate concern for optimization. -[^fn-raspberry-pi-development]: **Raspberry Pi Development Advantages**: Despite costing only $35-75, the Raspberry Pi 4 provides 1000$\times$ more RAM and 10$\times$ faster processing than typical production IoT devices. This substantial resource overhead enables developers to prototype using full Python frameworks like TensorFlow or PyTorch before optimizing for resource-constrained deployment. However, the Pi's 3-8W power consumption versus production devices' 0.1W creates a 30-80$\times$ power gap that requires significant optimization during transition to real-world deployment. +[^fn-raspberry-pi-development]: **Raspberry Pi Development Advantages**: Despite costing only $35-75, the Raspberry Pi 4 provides 1000 $\times$ more RAM and 10 $\times$ faster processing than typical production IoT devices. This substantial resource overhead enables developers to prototype using full Python frameworks like TensorFlow or PyTorch before optimizing for resource-constrained deployment. However, the Pi's 3-8W power consumption versus production devices' 0.1W creates a 30-80 $\times$ power gap that requires significant optimization during transition to real-world deployment. Production deployments reveal resource limitations that contrast with development environments. When scaling to thousands of devices, cost and power constraints often mandate the use of microcontroller units like the ESP32[^fn-esp32-constraints], a widely used microcontroller unit from Espressif Systems, with its 240 MHz dual-core processor and 520 KB total SRAM with 320-450 KB available depending on the variant. This dramatic reduction in computational resources demands changes in system architecture. On-device learning techniques become essential: models must be redesigned for constrained execution, optimization techniques such as quantization and pruning applied, inference strategies adapted for minimal memory footprints, and update mechanisms implemented that work within severe bandwidth and storage limitations. -[^fn-esp32-constraints]: **ESP32 Capabilities**: Despite its constraints, the ESP32 costs only $2-5, consumes 30-150mA during operation, and includes Wi-Fi, Bluetooth, and various sensors. This makes it ideal for IoT deployments in social impact applications. For comparison, a smartphone processor is 100$\times$ more powerful but costs 50$\times$ more. The ESP32's limitations (RAM smaller than a single Instagram photo) force engineers to develop optimization techniques that often benefit all platforms. +[^fn-esp32-constraints]: **ESP32 Capabilities**: Despite its constraints, the ESP32 costs only $2-5, consumes 30-150mA during operation, and includes Wi-Fi, Bluetooth, and various sensors. This makes it ideal for IoT deployments in social impact applications. For comparison, a smartphone processor is 100 $\times$ more powerful but costs 50 $\times$ more. The ESP32's limitations (RAM smaller than a single Instagram photo) force engineers to develop optimization techniques that often benefit all platforms. Beyond computational scaling, network infrastructure constraints significantly influence system architecture at scale. Different deployment contexts necessitate different communication protocols, each with distinct operational parameters. This heterogeneity in network infrastructure requires systems to maintain consistent performance across varying bandwidth and latency conditions. As deployments scale across regions, system architectures must accommodate seamless transitions between network technologies while preserving functionality. @@ -471,7 +471,7 @@ Staged rollouts with automatic rollback prevent deployment of defective models a Solar-powered deployments with cellular connectivity face strict bandwidth budgets imposed by data plan costs and energy consumption for radio transmission. A typical deployment might allocate 10-50 MB monthly bandwidth per device at $0.10-0.50 per MB in rural regions. Adaptive bandwidth allocation dynamically adjusts transmission behavior based on current budget consumption. Systems track daily bandwidth usage against pro-rated monthly allocation: if a device consumes 5 MB in the first week (targeting 2.5 MB), synchronization frequency reduces from hourly to 6-hour intervals. -Compression and quality adaptation trade data fidelity for bandwidth efficiency. Agricultural monitoring images captured at 2048$\times$1536 resolution (3-5 MB per image) compress to 512$\times$384 resolution with 70% JPEG quality (150-300 KB per image) for transmission, reducing bandwidth by 90% while preserving sufficient detail for crop disease detection. When bandwidth budget tightens further, systems reduce to 256$\times$256 resolution (50-80 KB) or extract features locally and transmit only 5-10 KB feature vectors. +Compression and quality adaptation trade data fidelity for bandwidth efficiency. Agricultural monitoring images captured at 2048 $\times$ 1536 resolution (3-5 MB per image) compress to 512 $\times$ 384 resolution with 70% JPEG quality (150-300 KB per image) for transmission, reducing bandwidth by 90% while preserving sufficient detail for crop disease detection. When bandwidth budget tightens further, systems reduce to 256 $\times$ 256 resolution (50-80 KB) or extract features locally and transmit only 5-10 KB feature vectors. **Resilience Validation and Testing** @@ -481,7 +481,7 @@ The engineering challenges and failure patterns described above demand more than ## Design Pattern Framework {#sec-ai-good-design-pattern-framework-36a6} -The engineering challenges detailed in @sec-ai-good-resource-constraints-engineering-challenges-a473 reveal three core constraints distinguishing social good deployments: communication bottlenecks (where data transmission costs exceed local computation), sample scarcity (creating 100-1000$\times$ gaps between theoretical requirements and available data), and energy limitations (forcing explicit accuracy-longevity trade-offs). +The engineering challenges detailed in @sec-ai-good-resource-constraints-engineering-challenges-a473 reveal three core constraints distinguishing social good deployments: communication bottlenecks (where data transmission costs exceed local computation), sample scarcity (creating 100-1000 $\times$ gaps between theoretical requirements and available data), and energy limitations (forcing explicit accuracy-longevity trade-offs). Rather than address these constraints ad-hoc, systematic design patterns provide principled architectural approaches. It is a fallacy to assume that resource-constrained deployments simply require "scaled-down" versions of cloud systems. As the design patterns show, they require different architectures optimized for specific constraint combinations rather than reduced functionality. @@ -833,7 +833,7 @@ Modern implementations of the progressive enhancement pattern incorporate automa Automated architecture optimization represents a significant advancement in implementing progressive enhancement layers. Contemporary systems employ Neural Architecture Search[^fn-nas] [@zoph2017neural] to generate model families optimized for specific resource constraints. -[^fn-nas]: **Neural Architecture Search (NAS)**: Automated methods for discovering optimal neural network architectures for specific constraints. For social good deployments, NAS can generate model families spanning 500KB to 50MB that share common components, enabling smooth transitions between enhancement layers. Hardware-aware NAS variants like MnasNet and FBNet explicitly optimize for latency on target devices, producing architectures that achieve 2-3$\times$ better efficiency than manually designed networks on edge hardware. The search process itself requires significant computation (1000+ GPU hours), but the resulting architectures can be deployed freely across resource-constrained environments. +[^fn-nas]: **Neural Architecture Search (NAS)**: Automated methods for discovering optimal neural network architectures for specific constraints. For social good deployments, NAS can generate model families spanning 500KB to 50MB that share common components, enabling smooth transitions between enhancement layers. Hardware-aware NAS variants like MnasNet and FBNet explicitly optimize for latency on target devices, producing architectures that achieve 2-3 $\times$ better efficiency than manually designed networks on edge hardware. The search process itself requires significant computation (1000+ GPU hours), but the resulting architectures can be deployed freely across resource-constrained environments. For example, a computer vision system might maintain multiple model variants ranging from 500 KB to 50 MB in size, each preserving maximum accuracy within its respective computational bounds. This automated approach ensures consistent performance scaling across enhancement layers, while setting the foundation for more sophisticated adaptation mechanisms. @@ -907,7 +907,7 @@ class ProgressiveHealthcareAI: TensorFlow Lite excels at creating optimized models for resource-constrained deployment layers. @lst-tflite-quantization presents a quantization pipeline that generates multiple model variants optimized for different enhancement tiers. -::: {#lst-tflite-quantization lst-cap="**Multi-Tier Quantization Pipeline**: Creates progressive enhancement layers using TensorFlow Lite quantization, achieving 4$\times$ size reduction with INT8 for baseline devices and 2$\times$ reduction with Float16 for intermediate devices while maintaining acceptable accuracy trade-offs."} +::: {#lst-tflite-quantization lst-cap="**Multi-Tier Quantization Pipeline**: Creates progressive enhancement layers using TensorFlow Lite quantization, achieving 4 $\times$ size reduction with INT8 for baseline devices and 2 $\times$ reduction with Float16 for intermediate devices while maintaining acceptable accuracy trade-offs."} ```{.python} # Quantization pipeline for progressive enhancement layers converter = tf.lite.TFLiteConverter.from_saved_model(model_path) @@ -1098,7 +1098,7 @@ Model architecture design requires specific adaptations for distributed deployme Training dynamics become particularly complex in Distributed Knowledge Pattern systems. Unlike centralized training approaches, these systems must implement collaborative learning mechanisms that function effectively across unreliable networks. Federated averaging protocols must be adapted to handle non-IID[^fn-non-iid] (non-Independent and Identically Distributed) data distributions across nodes while maintaining convergence guarantees. -[^fn-non-iid]: **Non-IID Data Distributions**: Traditional machine learning assumes training data is independently and identically distributed (IID), where each sample comes from the same underlying distribution. In social good deployments, this assumption fails systematically: health clinics in different regions see different disease patterns, agricultural sensors in varying microclimates generate distinct readings, and local camera traps capture species specific to their geography. This non-IID reality causes federated learning to converge 2-5$\times$ slower and may produce models biased toward data-rich nodes. Techniques like FedProx and personalized federated learning address this by allowing local model adaptation while maintaining global coordination. +[^fn-non-iid]: **Non-IID Data Distributions**: Traditional machine learning assumes training data is independently and identically distributed (IID), where each sample comes from the same underlying distribution. In social good deployments, this assumption fails systematically: health clinics in different regions see different disease patterns, agricultural sensors in varying microclimates generate distinct readings, and local camera traps capture species specific to their geography. This non-IID reality causes federated learning to converge 2-5 $\times$ slower and may produce models biased toward data-rich nodes. Techniques like FedProx and personalized federated learning address this by allowing local model adaptation while maintaining global coordination. Training procedures must also account for varying data qualities and quantities across nodes, implementing weighted aggregation schemes that reflect data reliability and relevance. @@ -1254,7 +1254,7 @@ Traditional supervised learning assumes abundant labeled data, typically requiri #### Few-Shot Learning Requirements {#sec-ai-good-fewshot-learning-requirements-fcee} -This challenge becomes concrete in applications like agricultural disease detection. While commercial crop monitoring systems train on millions of labeled images from controlled environments, rural deployments must identify diseases using fewer than 50 examples per disease class. This 20$\times$ reduction in training data requires learning approaches that leverage structural similarities across disease types and transfer knowledge from related domains. +This challenge becomes concrete in applications like agricultural disease detection. While commercial crop monitoring systems train on millions of labeled images from controlled environments, rural deployments must identify diseases using fewer than 50 examples per disease class. This 20 $\times$ reduction in training data requires learning approaches that leverage structural similarities across disease types and transfer knowledge from related domains. The theoretical gap becomes apparent when comparing learning curves. Traditional deep learning approaches require exponential data scaling to achieve linear improvements in accuracy, following power laws where accuracy ∝ (data_size)^α with α typically 0.1-0.3. Resource-constrained environments require learning algorithms that achieve α ≥ 0.7, approaching human-level sample efficiency where single examples can generalize to entire categories. @@ -1264,7 +1264,7 @@ The gap between available data and required sample complexity can be formalized ::: {.callout-note title="Mathematical Depth" collapse="false"} -This subsection uses computational learning theory (PAC-learning bounds) to formalize the sample complexity gap. Readers unfamiliar with complexity notation can focus on the key quantitative insight: traditional learning theory requires 100-1000$\times$ more training examples than resource-constrained environments typically provide. Understanding the specific mathematical bounds is not essential for the design patterns presented earlier. +This subsection uses computational learning theory (PAC-learning bounds) to formalize the sample complexity gap. Readers unfamiliar with complexity notation can focus on the key quantitative insight: traditional learning theory requires 100-1000 $\times$ more training examples than resource-constrained environments typically provide. Understanding the specific mathematical bounds is not essential for the design patterns presented earlier. ::: @@ -1272,7 +1272,7 @@ To quantify these limitations, PAC-learning theory provides bounds on minimum sa - **Traditional bound**: O(k $\times$ d / ε²) samples required for reliable classification - **Resource-constrained reality**: Often <50 samples per class available -- **Gap magnitude**: 100-1000$\times$ difference between theory and practice +- **Gap magnitude**: 100-1000 $\times$ difference between theory and practice Bridging this gap necessitates learning approaches that exploit additional structure in the problem domain, such as: @@ -1284,7 +1284,7 @@ Bridging this gap necessitates learning approaches that exploit additional struc Resource-constrained deployments face a fundamental annotation paradox: achieving acceptable model performance requires hundreds of labeled examples, yet expert annotation time costs far more than the communities can afford. A medical specialist charging $100 per hour reviewing diagnostic images at 5 minutes per image creates $33 per label annotation costs, making the 500-1000 labels required for traditional supervised learning economically infeasible for low-resource healthcare systems. -Active learning addresses this paradox by strategically selecting which examples to annotate, maximizing model improvement per annotation dollar spent. Rather than randomly labeling data, active learning algorithms identify examples where model uncertainty is highest, label disagreement occurs, or information gain would be maximum. This targeted approach reduces annotation requirements by 5-10$\times$ compared to random sampling while achieving equivalent model performance. +Active learning addresses this paradox by strategically selecting which examples to annotate, maximizing model improvement per annotation dollar spent. Rather than randomly labeling data, active learning algorithms identify examples where model uncertainty is highest, label disagreement occurs, or information gain would be maximum. This targeted approach reduces annotation requirements by 5-10 $\times$ compared to random sampling while achieving equivalent model performance. **Query Strategy Selection for Resource-Constrained Contexts** @@ -1298,7 +1298,7 @@ Quantitatively, uncertainty sampling achieves learning curves of N^0.6-0.7 compa Implementation under resource constraints requires balancing ensemble size against computational overhead. A practical deployment might maintain 3-5 diverse models (varying architectures, hyperparameters, or random initializations) rather than the 10-20 models used in unconstrained settings. This trades some query quality for feasible on-device computation, with disagreement measured via prediction entropy across the ensemble. -**Expected model change** selects examples predicted to cause largest gradient updates if labeled, directly optimizing for learning progress. This strategy requires computing expected gradients under different possible label assignments, creating computational overhead of 2-10$\times$ compared to uncertainty sampling. For resource-constrained deployments, this approach becomes feasible only at regional nodes with sufficient computation for gradient estimation, not on edge devices. +**Expected model change** selects examples predicted to cause largest gradient updates if labeled, directly optimizing for learning progress. This strategy requires computing expected gradients under different possible label assignments, creating computational overhead of 2-10 $\times$ compared to uncertainty sampling. For resource-constrained deployments, this approach becomes feasible only at regional nodes with sufficient computation for gradient estimation, not on edge devices. **Annotation Budget Allocation and Scheduling** @@ -1310,7 +1310,7 @@ Diversity-aware batch selection addresses this by jointly optimizing for uncerta **Annotation budget smoothing** distributes limited expert time effectively across deployment lifecycle. Consider a rural health clinic receiving 50 diagnostic images daily with a consulting specialist available for 2 hours monthly (approximately 24 annotations at 5 minutes each). A naive approach would accumulate 1500 monthly images and select 24 for labeling, creating temporal gaps where model improvement lags deployment needs. -An alternative approach implements continuous learning with weekly annotation cycles: accumulate 350 weekly images, select 6 most informative cases for specialist review, retrain model, and deploy updates before next cycle. This 4$\times$ higher update frequency enables model improvement to track distribution shifts and emerging disease patterns more responsively, though each update uses fewer labels. +An alternative approach implements continuous learning with weekly annotation cycles: accumulate 350 weekly images, select 6 most informative cases for specialist review, retrain model, and deploy updates before next cycle. This 4 $\times$ higher update frequency enables model improvement to track distribution shifts and emerging disease patterns more responsively, though each update uses fewer labels. **Quantitative analysis** reveals the trade-off: monthly batches achieve 2-3% higher accuracy given identical total annotations (24 examples enable better statistical coverage), but weekly updates reduce average model staleness from 15 days to 3.5 days. In domains where distribution shift is rapid (seasonal disease patterns, changing environmental conditions), the responsiveness gain outweighs the modest accuracy loss. @@ -1330,7 +1330,7 @@ Quantitatively, consensus strategies become cost-effective when: cost_consensus / quality_consensus < cost_expert / quality_expert -For the medical example: $15 / 0.80 = 18.75 versus $33 / 0.90 = 36.67, making consensus 2$\times$ more cost-effective despite requiring more total annotation time. +For the medical example: $15 / 0.80 = 18.75 versus $33 / 0.90 = 36.67, making consensus 2 $\times$ more cost-effective despite requiring more total annotation time. **Adaptive expertise allocation** dynamically routes examples to appropriate annotator skill levels based on predicted difficulty. Simple, unambiguous cases receive community annotator labels ($5), while ambiguous cases requiring specialist knowledge escalate to experts ($33). A practical system might implement: @@ -1346,11 +1346,11 @@ Implementing human-in-the-loop active learning in resource-constrained settings **Offline annotation interfaces** enable experts to review queued examples during connectivity windows or via periodic dataset synchronization. The system maintains a priority queue of unlabeled examples ranked by active learning scores, synchronized to expert devices when connectivity permits. Annotations flow back asynchronously, triggering model retraining when sufficient new labels accumulate (typically 10-50 examples depending on domain). -**Annotation bandwidth optimization** reduces synchronization overhead by transmitting only essential data. For image-based tasks, systems send downsampled images (512$\times$512 rather than full 2048$\times$2048 resolution) for annotation, reducing bandwidth from 3-5 MB to 150-300 KB per image while preserving diagnostic information. When experts require full resolution, they flag cases for high-resolution synchronization during the next connectivity window. +**Annotation bandwidth optimization** reduces synchronization overhead by transmitting only essential data. For image-based tasks, systems send downsampled images (512 $\times$ 512 rather than full 2048 $\times$ 2048 resolution) for annotation, reducing bandwidth from 3-5 MB to 150-300 KB per image while preserving diagnostic information. When experts require full resolution, they flag cases for high-resolution synchronization during the next connectivity window. **Incremental model updates** retrain models efficiently as new labels arrive. Rather than retraining from scratch with each annotation batch (computationally expensive), systems implement online or incremental learning algorithms that update model parameters using only new examples. For neural networks, this might involve fine-tuning the final layers while freezing earlier layers, reducing training time from hours to minutes and enabling more responsive model improvement cycles. -These human-in-the-loop active learning patterns address the annotation paradox by extracting maximum value from scarce expert time. Deployments implementing these strategies achieve model performance comparable to fully supervised baselines while reducing annotation requirements by 5-10$\times$ and costs by similar factors, making expert-quality ML systems economically viable in resource-constrained contexts. +These human-in-the-loop active learning patterns address the annotation paradox by extracting maximum value from scarce expert time. Deployments implementing these strategies achieve model performance comparable to fully supervised baselines while reducing annotation requirements by 5-10 $\times$ and costs by similar factors, making expert-quality ML systems economically viable in resource-constrained contexts. ### Learning Without Labeled Data {#sec-ai-good-learning-without-labeled-data-88fb} @@ -1360,11 +1360,11 @@ Building on these sample complexity challenges, resource-constrained environment Contrastive approaches[^fn-contrastive-learning] learn representations by distinguishing between similar and dissimilar examples without requiring explicit labels [@chen2020simple]. From a systems engineering perspective, this impacts deployment architecture in several ways. -[^fn-contrastive-learning]: **Contrastive Learning**: A self-supervised learning approach where models learn to recognize similarities and differences between data points without labeled examples. The model learns that augmented versions of the same image should have similar representations while different images should have dissimilar ones. For social good, this enables pretraining on abundant unlabeled data (thousands of crop images) before fine-tuning on scarce labeled examples (50 disease-labeled samples), reducing data requirements by 5-15$\times$ compared to training from scratch. +[^fn-contrastive-learning]: **Contrastive Learning**: A self-supervised learning approach where models learn to recognize similarities and differences between data points without labeled examples. The model learns that augmented versions of the same image should have similar representations while different images should have dissimilar ones. For social good, this enables pretraining on abundant unlabeled data (thousands of crop images) before fine-tuning on scarce labeled examples (50 disease-labeled samples), reducing data requirements by 5-15 $\times$ compared to training from scratch. Edge devices can collect unlabeled data continuously during normal operation, building local datasets without expensive annotation. Regional servers can then perform contrastive pretraining on aggregated unlabeled data, creating foundation models that edge devices download and fine-tune with their limited labeled examples. -This architectural pattern reduces the sample complexity burden by factors of 5-15$\times$ compared to training from scratch. For a crop monitoring system, this means a deployment can achieve 87% disease detection accuracy with fewer than 50 labeled examples per disease class, provided it has access to thousands of unlabeled field images. The systems challenge becomes managing this two-stage pipeline (unsupervised pretraining at regional scale followed by supervised fine-tuning at edge scale) within bandwidth and compute constraints. +This architectural pattern reduces the sample complexity burden by factors of 5-15 $\times$ compared to training from scratch. For a crop monitoring system, this means a deployment can achieve 87% disease detection accuracy with fewer than 50 labeled examples per disease class, provided it has access to thousands of unlabeled field images. The systems challenge becomes managing this two-stage pipeline (unsupervised pretraining at regional scale followed by supervised fine-tuning at edge scale) within bandwidth and compute constraints. #### Mutual Information Bounds {#sec-ai-good-mutual-information-bounds-7165} @@ -1498,7 +1498,7 @@ Technical teams often assume that local implementing partners will resolve the " **Pitfall: Optimizing for pilot success instead of sustainable scale.** -Pilot deployments receive intensive support: dedicated staff visiting users weekly, rapid troubleshooting responses within hours, motivated early-adopter users, and flexible timelines. These conditions systematically differ from scale deployments where support ratios drop from 1:50 to 1:5000, response times extend to days or weeks, users are less engaged, and operational constraints are rigid. The economic reality is stark: pilot support costs $200-500 per user annually, while sustainable operations must operate at $5-20 per user to remain viable. A common pattern observed across agricultural, health, and education deployments: pilots achieve 80% adoption rates with intensive support, organizations declare success and scale to 10-20$\times$ more users, scaled deployments achieve 15% adoption without intensive support, and projects are quietly abandoned within 18-24 months. As discussed in @sec-ai-good-common-deployment-failures-sociotechnical-pitfalls-966b, development and production environments differ fundamentally. Sustainable deployments design for minimal-support operation from the beginning, treating pilot intensive support as a bug rather than a feature. +Pilot deployments receive intensive support: dedicated staff visiting users weekly, rapid troubleshooting responses within hours, motivated early-adopter users, and flexible timelines. These conditions systematically differ from scale deployments where support ratios drop from 1:50 to 1:5000, response times extend to days or weeks, users are less engaged, and operational constraints are rigid. The economic reality is stark: pilot support costs $200-500 per user annually, while sustainable operations must operate at $5-20 per user to remain viable. A common pattern observed across agricultural, health, and education deployments: pilots achieve 80% adoption rates with intensive support, organizations declare success and scale to 10-20 $\times$ more users, scaled deployments achieve 15% adoption without intensive support, and projects are quietly abandoned within 18-24 months. As discussed in @sec-ai-good-common-deployment-failures-sociotechnical-pitfalls-966b, development and production environments differ fundamentally. Sustainable deployments design for minimal-support operation from the beginning, treating pilot intensive support as a bug rather than a feature. **Fallacy: Open-sourcing ensures sustainability.** @@ -1514,7 +1514,7 @@ Building secure, accurate, privacy-preserving systems does not prevent harmful a **Pitfall: Measuring success at deployment rather than adoption.** -Organizations commonly declare success when systems are deployed, installed, or launched. Actual impact requires sustained adoption: users continuing to engage over months and years, not just during initial rollout. The distinction is quantifiable and dramatic. Analysis of AI for Good project outcomes across 80+ deployments reveals a consistent pattern: initial deployment reports frequently claim 60-80% user engagement success, yet follow-up assessments at 12 months show 25-35% sustained adoption, and at 24 months only 10-20% maintain regular usage. This 4-8$\times$ drop represents systems that achieved technical deployment (software installed, hardware distributed, training completed) without achieving lasting impact (behavior change, outcome improvements). Conference papers and funding reports celebrate launch milestones while actual community benefit remains unrealized. Success metrics should track 12-month and 24-month adoption rates, user retention curves, and outcome improvements, not launch statistics or pilot enrollment numbers. +Organizations commonly declare success when systems are deployed, installed, or launched. Actual impact requires sustained adoption: users continuing to engage over months and years, not just during initial rollout. The distinction is quantifiable and dramatic. Analysis of AI for Good project outcomes across 80+ deployments reveals a consistent pattern: initial deployment reports frequently claim 60-80% user engagement success, yet follow-up assessments at 12 months show 25-35% sustained adoption, and at 24 months only 10-20% maintain regular usage. This 4-8 $\times$ drop represents systems that achieved technical deployment (software installed, hardware distributed, training completed) without achieving lasting impact (behavior change, outcome improvements). Conference papers and funding reports celebrate launch milestones while actual community benefit remains unrealized. Success metrics should track 12-month and 24-month adoption rates, user retention curves, and outcome improvements, not launch statistics or pilot enrollment numbers. ## Summary {#sec-ai-good-summary-2437} @@ -1527,7 +1527,7 @@ We established that social impact requires an "offline-first" mindset, using sys * **The Ultimate Synthesis**: Social impact deployment is the highest level of ML systems engineering. It requires perfectly balancing extreme quantization, adaptive power management, and federated coordination within a single, reliable service. * **Bridging the Digital Language Divide**: True "AI for All" requires solving the data scarcity of low-resource languages. Success depends on cross-lingual transfer, synthetic data, and deploying Intelligent Tutoring Systems on $50 hardware. * **The Resource Paradox**: Infrastructure is rarest where need is highest. Engineers must design systems assuming zero connectivity as the baseline, treating network access as an enhancement rather than a requirement. -* **From Predictive to Prescriptive**: The next frontier of impact is **Simulation Intelligence**. By using AI surrogates to model climate, policy, and health outcomes 1000$\times$ faster than traditional methods, we can evaluate interventions before they are deployed. +* **From Predictive to Prescriptive**: The next frontier of impact is **Simulation Intelligence**. By using AI surrogates to model climate, policy, and health outcomes 1000 $\times$ faster than traditional methods, we can evaluate interventions before they are deployed. * **Sociotechnical Humility**: Technical accuracy does not equal impact. A model that is 99% accurate but culturally misaligned or infrastructure-dependent will deliver zero benefit. Social good requires deep community co-design. ::: diff --git a/book/quarto/contents/vol2/backmatter/glossary/glossary.qmd b/book/quarto/contents/vol2/backmatter/glossary/glossary.qmd index e8a9f3fb06..149efdb9a9 100644 --- a/book/quarto/contents/vol2/backmatter/glossary/glossary.qmd +++ b/book/quarto/contents/vol2/backmatter/glossary/glossary.qmd @@ -240,7 +240,7 @@ This glossary contains definitions of key terms used in Volume II: Scalable Syst *Appears in: @sec-sustainable-ai* **depthwise separable convolutions** -: A computational technique that decomposes standard convolutions into depthwise and pointwise operations, reducing parameters and computation by 8-9$\times$ for mobile-optimized architectures. +: A computational technique that decomposes standard convolutions into depthwise and pointwise operations, reducing parameters and computation by 8-9 $\times$ for mobile-optimized architectures. *Appears in: @sec-edge-intelligence-ondevice-learning-benefits-3256* **differential privacy** diff --git a/book/quarto/contents/vol2/communication_ops/communication_ops.qmd b/book/quarto/contents/vol2/communication_ops/communication_ops.qmd index d7a5b98687..e6fead858d 100644 --- a/book/quarto/contents/vol2/communication_ops/communication_ops.qmd +++ b/book/quarto/contents/vol2/communication_ops/communication_ops.qmd @@ -438,10 +438,10 @@ The final challenge for our gradient is the physical layout of the datacenter. N Real clusters are **hierarchical**, with fundamentally different bandwidths at each tier, as @tbl-bandwidth-hierarchy quantifies: -| **Tier** | **Interconnect** | **Bandwidth** | **Relative Speed** | -|:---------------|--------------------:|--------------:|---------------------:| -| **Intra-Node** | NVLink 4.0 | ~900 GB/s | 18$\times$ faster | -| **Inter-Node** | InfiniBand NDR 400G | ~50 GB/s | 1$\times$ (baseline) | +| **Tier** | **Interconnect** | **Bandwidth** | **Relative Speed** | +|:---------------|--------------------:|--------------:|----------------------:| +| **Intra-Node** | NVLink 4.0 | ~900 GB/s | 18 $\times$ faster | +| **Inter-Node** | InfiniBand NDR 400G | ~50 GB/s | 1 $\times$ (baseline) | : **The Bandwidth Hierarchy**: Modern GPU clusters exhibit an order-of-magnitude bandwidth gap between intra-node and inter-node communication. Hierarchical algorithms exploit this gap. {#tbl-bandwidth-hierarchy} @@ -526,7 +526,7 @@ speedup_str = f"{speedup:.1f}" 3. **Intra-Node AllGather**: Each GPU receives `{python} intra_data_mb_str` MB at 900 GB/s → ~`{python} intra_ag_ms_str` ms - **Total Time**: $\approx 1 + 5 + 1 = 7\ \text{ms}$ -**The Systems Insight**: Hierarchical AllReduce achieves a **`{python} speedup_str`$\times$ speedup** by reducing inter-node traffic from 1 GB to `{python} inter_data_mb_str` MB per GPU. With 8 GPUs per node, we effectively get $8\times$ the apparent inter-node bandwidth. This is why NVIDIA's NCCL and similar libraries default to hierarchical algorithms on multi-node clusters. +**The Systems Insight**: Hierarchical AllReduce achieves a **`{python} speedup_str` $\times$ speedup** by reducing inter-node traffic from 1 GB to `{python} inter_data_mb_str` MB per GPU. With 8 GPUs per node, we effectively get $8\times$ the apparent inter-node bandwidth. This is why NVIDIA's NCCL and similar libraries default to hierarchical algorithms on multi-node clusters. ::: These three phases confine most traffic within each node before crossing the slower inter-node fabric. @@ -632,7 +632,7 @@ This creates 8 independent "rails" of communication that operate in parallel wit : **Rail-Optimized Traffic Distribution**: Each rail carries 1/8 of the total traffic independently. {#tbl-rail-optimized} -**Why Rails Matter**: Without rail alignment, all 8 GPUs on a node might try to send to the same remote GPU simultaneously, creating 8$\times$ contention on a single NIC. Rail-aligned routing ensures each NIC handles exactly 1/8 of the traffic, achieving full bisection bandwidth utilization. +**Why Rails Matter**: Without rail alignment, all 8 GPUs on a node might try to send to the same remote GPU simultaneously, creating 8 $\times$ contention on a single NIC. Rail-aligned routing ensures each NIC handles exactly 1/8 of the traffic, achieving full bisection bandwidth utilization. ## The Last Resort: Gradient Compression {#sec-communication-collective-operations-collective-operations-gradient-compression-7a5c} @@ -644,11 +644,11 @@ Most gradients are computed in FP32 (32-bit floating point) or BF16 (16-bit brai **The Quantization Progression:** -1. **FP16 (16-bit)**: The baseline for modern training. Half the bits of FP32, with minimal impact on convergence for most models. Provides **2$\times$ compression** over FP32. +1. **FP16 (16-bit)**: The baseline for modern training. Half the bits of FP32, with minimal impact on convergence for most models. Provides **2 $\times$ compression** over FP32. -2. **INT8 (8-bit)**: Quantize each gradient vector to 256 discrete levels. This requires computing a **scaling factor** per tensor: $g_{int8} = \text{round}(g / s)$ where $s = \max(|g|) / 127$. The receiver reconstructs $\hat{g} = g_{int8} \times s$. Provides **4$\times$ compression** over FP32 but introduces quantization noise proportional to the gradient magnitude. +2. **INT8 (8-bit)**: Quantize each gradient vector to 256 discrete levels. This requires computing a **scaling factor** per tensor: $g_{int8} = \text{round}(g / s)$ where $s = \max(|g|) / 127$. The receiver reconstructs $\hat{g} = g_{int8} \times s$. Provides **4 $\times$ compression** over FP32 but introduces quantization noise proportional to the gradient magnitude. -3. **1-bit SGD**: The extreme case—transmit only the **sign** of each gradient element (+1 or -1). The receiver reconstructs using a learned or adaptive scaling factor. Provides **32$\times$ compression** over FP32 but introduces substantial noise that typically degrades convergence without additional mechanisms. +3. **1-bit SGD**: The extreme case—transmit only the **sign** of each gradient element (+1 or -1). The receiver reconstructs using a learned or adaptive scaling factor. Provides **32 $\times$ compression** over FP32 but introduces substantial noise that typically degrades convergence without additional mechanisms. **Trade-off**: Each reduction in bit-width introduces quantization noise. This noise acts as a biased perturbation to the true gradient direction. For aggressive quantization (INT8 and especially 1-bit), the systematic bias can prevent convergence unless corrected by **Error Feedback** (see below). @@ -662,7 +662,7 @@ The most common method is **Top-K compression**: for a gradient vector $g \in \m $$\text{TopK}(g) = g \odot \mathbf{1}_{|g| \geq |g|_{(K)}}$$ -where $|g|_{(K)}$ is the $K$-th largest element by magnitude. With $K = 0.001 \times d$ (keeping only 0.1% of elements), this achieves **1000$\times$ compression**. +where $|g|_{(K)}$ is the $K$-th largest element by magnitude. With $K = 0.001 \times d$ (keeping only 0.1% of elements), this achieves **1000 $\times$ compression**. **The Convergence Problem:** @@ -738,10 +738,10 @@ Gradient compression is not free—it trades reduced communication for increased | **Method** | **Compression Ratio** | **Convergence Impact** | **Best Use Case** | |:--------------------------|----------------------:|-------------------------------:|:-------------------------------| -| **FP16** | 2$\times$ | Negligible | Default for all training | -| **INT8 + Error FB** | 4$\times$ | Minor slowdown (~5-10%) | Bandwidth-constrained clusters | -| **Top-K (1%) + Error FB** | 100$\times$ | Moderate slowdown (~10-20%) | Cross-datacenter training | -| **1-bit + Error FB** | 32$\times$ | Significant slowdown (~20-30%) | Extreme bandwidth constraints | +| **FP16** | 2 $\times$ | Negligible | Default for all training | +| **INT8 + Error FB** | 4 $\times$ | Minor slowdown (~5-10%) | Bandwidth-constrained clusters | +| **Top-K (1%) + Error FB** | 100 $\times$ | Moderate slowdown (~10-20%) | Cross-datacenter training | +| **1-bit + Error FB** | 32 $\times$ | Significant slowdown (~20-30%) | Extreme bandwidth constraints | **When to Use Compression:** @@ -760,10 +760,10 @@ Three libraries dominate distributed ML communication. Understanding their trade NVIDIA Collective Communications Library (NCCL)[^fn-nccl] is the de-facto standard for multi-GPU training. Its dominance stems from three GPU-specific optimizations that MPI and Gloo cannot replicate without hardware vendor support: * **Kernel Fusion**[^fn-kernel-fusion]: NCCL fuses the reduction operator (sum, average) directly into the memory copy kernel. Rather than copying data to a buffer, reducing, then copying results back, NCCL performs the reduction *during* the transfer. This eliminates intermediate memory traffic and maximizes HBM bandwidth utilization. -* **Channel Pipelining**: NCCL opens multiple parallel communication channels to saturate all available network interfaces simultaneously. A DGX node with 8 NICs can achieve 8$\times$ the bandwidth of a single-channel implementation by spreading the collective across all links. +* **Channel Pipelining**: NCCL opens multiple parallel communication channels to saturate all available network interfaces simultaneously. A DGX node with 8 NICs can achieve 8 $\times$ the bandwidth of a single-channel implementation by spreading the collective across all links. * **GPUDirect RDMA**[^fn-gpudirect]: The network card reads directly from GPU memory via PCIe, bypassing the CPU entirely. Without GPUDirect, data must traverse GPU → CPU memory → NIC → network, adding microseconds of latency and consuming CPU cycles. GPUDirect eliminates this overhead. -These optimizations explain why NCCL achieves 2-5$\times$ better performance than generic MPI implementations on GPU clusters. +These optimizations explain why NCCL achieves 2-5 $\times$ better performance than generic MPI implementations on GPU clusters. [^fn-nccl]: **NCCL (NVIDIA Collective Communications Library)**: NVIDIA's GPU-optimized communication library, first released in 2015. NCCL implements collective operations (AllReduce, AllGather, etc.) with GPU-aware optimizations that leverage NVLink, NVSwitch, and GPUDirect RDMA. diff --git a/book/quarto/contents/vol2/conclusion/conclusion.qmd b/book/quarto/contents/vol2/conclusion/conclusion.qmd index 90d5c82881..5620361596 100644 --- a/book/quarto/contents/vol2/conclusion/conclusion.qmd +++ b/book/quarto/contents/vol2/conclusion/conclusion.qmd @@ -96,7 +96,7 @@ Understanding this complete stack enables informed decisions at every level, fro [^fn-ring-allreduce]: Ring AllReduce, detailed in @sec-communication-collective-operations, achieves $2(n-1)/n$ bandwidth utilization for $n$ workers, enabling efficient gradient synchronization across large clusters. -[^fn-gradient-compression]: @sec-communication-collective-operations explores gradient compression techniques that reduce communication volume 10-100$\times$ through sparsification, quantization, and error feedback, enabling bandwidth-limited distributed and federated training. +[^fn-gradient-compression]: @sec-communication-collective-operations explores gradient compression techniques that reduce communication volume 10-100 $\times$ through sparsification, quantization, and error feedback, enabling bandwidth-limited distributed and federated training. Communication systems, however, are only as valuable as they are reliable. Network failures, node crashes, and synchronization breakdowns introduce a second fundamental constraint that shapes distributed ML engineering. @@ -163,7 +163,7 @@ At the highest level of the Systems Sandwich, you can address governance and eth These competencies equip you for today's challenges. But mastery of current systems is only valuable if you can adapt as the landscape shifts. We stand at the end of the **Era of Scaling**, where progress came from simply making models bigger, and at the beginning of the **Era of Composition**. This transition gives rise to what we term the *compound capability law*. ::: {.callout-perspective title="Compound Capability Law"} -**The Observation:** Moore's Law (transistor density) provided the first wave of exponential growth. Neural Scaling Laws (data/compute) provided the second. Both are now flattening against physical power limits. Where does the next 100$\times$ improvement come from? +**The Observation:** Moore's Law (transistor density) provided the first wave of exponential growth. Neural Scaling Laws (data/compute) provided the second. Both are now flattening against physical power limits. Where does the next 100 $\times$ improvement come from? **The Principle:** > *When individual model scaling saturates, system capability scales with the **complexity of orchestration**.* diff --git a/book/quarto/contents/vol2/distributed_training/distributed_training.qmd b/book/quarto/contents/vol2/distributed_training/distributed_training.qmd index ef43dffb1f..13a3b25b0c 100644 --- a/book/quarto/contents/vol2/distributed_training/distributed_training.qmd +++ b/book/quarto/contents/vol2/distributed_training/distributed_training.qmd @@ -501,7 +501,7 @@ Interconnect selection proves critical for large-scale deployments, as these com The bandwidth requirements for efficient distributed training are substantial, particularly for transformer models. Efficient systems require 100-400 GB/s aggregate bandwidth per node for transformer architectures. BERT-Base (110M parameters) requires approximately 440 MB of gradient synchronization per iteration in FP32, while BERT-Large (340M parameters) requires approximately 1.4 GB. Across 64 GPUs, these synchronization demands require 100-200 GB/s sustained bandwidth for sub-50ms synchronization latency. Language models with `{python} gpt3_params_b`B parameters require 700 GB/s aggregate bandwidth to maintain 80% parallel efficiency, necessitating InfiniBand HDR or equivalent interconnects. -Synchronization frequency presents a trade-off between communication efficiency and convergence behavior. Gradient accumulation reduces synchronization frequency but increases memory requirements and may impact convergence. Synchronizing every 4 steps reduces communication overhead by 60% while increasing memory usage by 3$\times$ for gradient storage. Asynchronous methods eliminate synchronization costs entirely but introduce staleness that degrades convergence by 15-30% for large learning rates. +Synchronization frequency presents a trade-off between communication efficiency and convergence behavior. Gradient accumulation reduces synchronization frequency but increases memory requirements and may impact convergence. Synchronizing every 4 steps reduces communication overhead by 60% while increasing memory usage by 3 $\times$ for gradient storage. Asynchronous methods eliminate synchronization costs entirely but introduce staleness that degrades convergence by 15-30% for large learning rates. ### The Physics of Scaling: Amdahl's Law with Communication {#sec-distributed-training-systems-systems-physics-scaling-amdahls-law-communication-4d7f} @@ -746,7 +746,7 @@ The scaling law regime exhibits three distinct behaviors: xmin=100, xmax=1000000, ymin=0.1, ymax=2, ytick={0.2, 0.5, 1.0, 2.0}, - yticklabels={0.2$\times$, 0.5$\times$, 1.0$\times$, 2.0$\times$}, + yticklabels={0.2 $\times$, 0.5 $\times$, 1.0 $\times$, 2.0 $\times$}, grid=major, legend pos=north east, legend style={font=\footnotesize}, @@ -822,12 +822,12 @@ To illustrate these concepts concretely, consider *scaling from 8 to 64 workers* | **Configuration** | **Iterations** | **Comm. Overhead** | **Wall-clock Speedup** | **Sample Efficiency** | |:---------------------|---------------:|-------------------:|-----------------------:|----------------------:| -| **1 GPU (baseline)** | 100,000 | 0% | 1.0$\times$ | 100% | -| **8 GPU BSP** | 12,800 | 15% | 6.8$\times$ | 97% | -| **64 GPU BSP** | 1,720 | 45% | 40.1$\times$ | 91% | -| **64 GPU SSP** | 2,100 | 25% | 38.1$\times$ | 74% | +| **1 GPU (baseline)** | 100,000 | 0% | 1.0 $\times$ | 100% | +| **8 GPU BSP** | 12,800 | 15% | 6.8 $\times$ | 97% | +| **64 GPU BSP** | 1,720 | 45% | 40.1 $\times$ | 91% | +| **64 GPU SSP** | 2,100 | 25% | 38.1 $\times$ | 74% | -The 64-GPU BSP configuration achieves 40$\times$ speedup despite only 91% sample efficiency because the communication overhead (45%) is offset by the massive parallelism. SSP provides comparable wall-clock time with lower communication overhead but requires more total samples. +The 64-GPU BSP configuration achieves 40 $\times$ speedup despite only 91% sample efficiency because the communication overhead (45%) is offset by the massive parallelism. SSP provides comparable wall-clock time with lower communication overhead but requires more total samples. **Cost Analysis** (assuming \$3/GPU-hour): @@ -835,7 +835,7 @@ The 64-GPU BSP configuration achieves 40$\times$ speedup despite only 91% sample - 64 GPU BSP: 1.72K iters $\times$ 0.58s/iter $\times$ 64 GPUs / SEC_PER_HOUR $\times$ \$3 = \$53 - 64 GPU SSP: 2.1K iters $\times$ 0.50s/iter $\times$ 64 GPUs / SEC_PER_HOUR $\times$ \$3 = \$56 -Despite higher parallelism, 64-GPU training costs more per run due to communication overhead and reduced sample efficiency. The 8-GPU configuration is more cost-efficient but takes 6$\times$ longer wall-clock time. The choice depends on whether minimizing cost or minimizing time-to-result is the priority. +Despite higher parallelism, 64-GPU training costs more per run due to communication overhead and reduced sample efficiency. The 8-GPU configuration is more cost-efficient but takes 6 $\times$ longer wall-clock time. The choice depends on whether minimizing cost or minimizing time-to-result is the priority. ::: @@ -889,7 +889,7 @@ The fundamental trade-off in distributed training is between communication effic Several techniques occupy different positions on this trade-off curve: -**Gradient Compression**: Reduces communication volume by 10-100$\times$ through quantization or sparsification, with 2-5% convergence degradation. Techniques like QSGD [@alistarh2017qsgd] and Top-K sparsification maintain convergence guarantees with bounded compression error. +**Gradient Compression**: Reduces communication volume by 10-100 $\times$ through quantization or sparsification, with 2-5% convergence degradation. Techniques like QSGD [@alistarh2017qsgd] and Top-K sparsification maintain convergence guarantees with bounded compression error. **Local SGD**: Workers perform $H$ local updates before synchronizing, reducing communication frequency by factor $H$. Convergence analysis shows that for smooth, strongly convex objectives, Local SGD achieves the same asymptotic rate as synchronous SGD with appropriately tuned learning rates [@stich2019local]. @@ -1265,7 +1265,7 @@ training_8gpu_str = f"{training_hours_8gpu:.1f}" ``` - Total: `{python} total_8gpu_str`ms per step -- Speedup: `{python} single_gpu_step_s_str`s ÷ `{python} total_8gpu_s_str`s = `{python} speedup_8gpu_str`$\times$ (not quite 8$\times$) +- Speedup: `{python} single_gpu_step_s_str`s ÷ `{python} total_8gpu_s_str`s = `{python} speedup_8gpu_str` $\times$ (not quite 8 $\times$) - Parallel efficiency: `{python} speedup_8gpu_str` ÷ 8 = `{python} efficiency_8gpu_str`% Why over 100% efficiency? Larger global batch (128 vs 16) improves GPU utilization from 72% to 89%. This "super-linear" speedup is common in ML at small scales when the baseline has poor utilization. @@ -1310,7 +1310,7 @@ training_32gpu_str = f"{training_32gpu_hours:.1f}" - Computation: `{python} compute_32gpu_ms`ms (`{python} compute_pct_str`% of time) - Communication: `{python} comm_32gpu_ms`ms (`{python} comm_pct_str`% of time) - Total: `{python} total_32gpu_str`ms per step -- Speedup: `{python} single_gpu_step_s_str`s ÷ `{python} total_32gpu_s_str`s = `{python} speedup_32gpu_str`$\times$ faster → `{python} training_32gpu_str` hours +- Speedup: `{python} single_gpu_step_s_str`s ÷ `{python} total_32gpu_s_str`s = `{python} speedup_32gpu_str` $\times$ faster → `{python} training_32gpu_str` hours - Parallel efficiency: `{python} speedup_32gpu_str` ÷ 32 = `{python} efficiency_32gpu_str`% Communication dominates and becomes the bottleneck. @@ -2201,7 +2201,7 @@ Distributed training involves counterintuitive behavior that leads to common mis **Fallacy:** ***Linear speedup is achievable with sufficient engineering effort.*** -Amdahl's Law establishes hard limits: any sequential component bounds maximum speedup regardless of parallelism. In distributed training, gradient synchronization is inherently sequential since all gradients must be collected before any update proceeds. As @sec-distributed-training-systems-efficiency-metrics demonstrates, the scaling efficiency equation $\text{Efficiency}(N) = 1/(1 + N(T_{comm}(N) - T_{overlap})/T_{compute})$ reveals how communication overhead dominates as $N$ increases. Even with perfect overlap and optimal algorithms, communication overhead grows with cluster size. For data parallelism, AllReduce time increases logarithmically with tree algorithms or linearly in the latency term with ring algorithms as GPU count grows. A 1000-GPU cluster will never train 1000$\times$ faster than a single GPU; achieving 500$\times$ speedup would be exceptional, and 100-200$\times$ is more typical for communication-heavy workloads. Organizations that budget projects assuming linear scaling inevitably miss deadlines and overspend on compute. +Amdahl's Law establishes hard limits: any sequential component bounds maximum speedup regardless of parallelism. In distributed training, gradient synchronization is inherently sequential since all gradients must be collected before any update proceeds. As @sec-distributed-training-systems-efficiency-metrics demonstrates, the scaling efficiency equation $\text{Efficiency}(N) = 1/(1 + N(T_{comm}(N) - T_{overlap})/T_{compute})$ reveals how communication overhead dominates as $N$ increases. Even with perfect overlap and optimal algorithms, communication overhead grows with cluster size. For data parallelism, AllReduce time increases logarithmically with tree algorithms or linearly in the latency term with ring algorithms as GPU count grows. A 1000-GPU cluster will never train 1000 $\times$ faster than a single GPU; achieving 500 $\times$ speedup would be exceptional, and 100-200 $\times$ is more typical for communication-heavy workloads. Organizations that budget projects assuming linear scaling inevitably miss deadlines and overspend on compute. **Pitfall:** ***Hyperparameters tuned on small clusters transfer directly to large-scale training.*** @@ -2213,7 +2213,7 @@ Beyond the critical batch size (model and dataset dependent, often 8K to 32K for **Fallacy:** ***Data parallelism scales indefinitely by adding more GPUs.*** -Engineers assume more GPUs always accelerate training. In production, statistical efficiency limits overwhelm hardware gains. As @sec-distributed-training-systems-data-parallelism establishes, data parallelism increases effective batch size proportionally with GPU count ($B_{total} = N \times B_{local}$), but gradient quality grows sublinearly beyond model-specific thresholds. A 100K-sample batch may provide only 2$\times$ the gradient information of a 10K-sample batch, not 10x, because samples become redundant within the loss landscape. The critical batch size defines where marginal returns collapse: for BERT-Base it occurs near 8K samples, for ResNet-50 near 32K samples. Beyond this threshold, doubling GPU count doubles cost but provides minimal convergence acceleration. A major cloud provider trained a large language model using 1024 GPUs that converged in 18 hours at $45,000 compute cost; the same model on 512 GPUs converged in 19 hours at $22,000 cost, demonstrating how exceeding critical batch size wastes resources without meaningful time savings. +Engineers assume more GPUs always accelerate training. In production, statistical efficiency limits overwhelm hardware gains. As @sec-distributed-training-systems-data-parallelism establishes, data parallelism increases effective batch size proportionally with GPU count ($B_{total} = N \times B_{local}$), but gradient quality grows sublinearly beyond model-specific thresholds. A 100K-sample batch may provide only 2 $\times$ the gradient information of a 10K-sample batch, not 10x, because samples become redundant within the loss landscape. The critical batch size defines where marginal returns collapse: for BERT-Base it occurs near 8K samples, for ResNet-50 near 32K samples. Beyond this threshold, doubling GPU count doubles cost but provides minimal convergence acceleration. A major cloud provider trained a large language model using 1024 GPUs that converged in 18 hours at $45,000 compute cost; the same model on 512 GPUs converged in 19 hours at $22,000 cost, demonstrating how exceeding critical batch size wastes resources without meaningful time savings. **Pitfall:** ***Choosing parallelism strategy based solely on memory constraints.*** @@ -2225,11 +2225,11 @@ Engineers adopt FSDP (Fully Sharded Data Parallel) universally after reading tha **Fallacy:** ***Parallelism overhead is roughly constant regardless of model size.*** -Engineers benchmark parallelism strategies on convenient small models then apply conclusions to large-scale training. In production, the ratio between computation and communication time changes dramatically with model size, inverting strategic decisions. AllReduce communication time depends primarily on gradient tensor size and network bandwidth, growing roughly linearly with parameter count, while forward and backward pass computation time grows superlinearly due to larger matrix operations. For a 1B parameter model where forward/backward pass takes 50ms and AllReduce takes 25ms, communication overhead consumes 33% of step time. For a 70B parameter model where forward/backward takes 2400ms and AllReduce takes 180ms, communication overhead drops to 7% despite the gradient size being 70$\times$ larger. Decisions made on small models ("pipeline parallelism's 15% bubble overhead makes it always slower than data parallelism") completely invert at scale where data parallelism's communication overhead reaches 25-40%. Reliable strategy selection requires either profiling at target scale or analytical models that account for how computation scales as $O(n^2)$ to $O(n^3)$ while communication scales as $O(n)$. +Engineers benchmark parallelism strategies on convenient small models then apply conclusions to large-scale training. In production, the ratio between computation and communication time changes dramatically with model size, inverting strategic decisions. AllReduce communication time depends primarily on gradient tensor size and network bandwidth, growing roughly linearly with parameter count, while forward and backward pass computation time grows superlinearly due to larger matrix operations. For a 1B parameter model where forward/backward pass takes 50ms and AllReduce takes 25ms, communication overhead consumes 33% of step time. For a 70B parameter model where forward/backward takes 2400ms and AllReduce takes 180ms, communication overhead drops to 7% despite the gradient size being 70 $\times$ larger. Decisions made on small models ("pipeline parallelism's 15% bubble overhead makes it always slower than data parallelism") completely invert at scale where data parallelism's communication overhead reaches 25-40%. Reliable strategy selection requires either profiling at target scale or analytical models that account for how computation scales as $O(n^2)$ to $O(n^3)$ while communication scales as $O(n)$. **Pitfall:** ***Gradient accumulation is free.*** -Engineers use gradient accumulation to simulate larger batch sizes, reducing synchronization frequency from every step to every $K$ steps. The technique appears cost-free since it eliminates $(K-1)/K$ of communication overhead. In production, accumulation introduces memory consumption, latency expansion, and numerical precision risks. Accumulated gradients consume additional memory throughout the accumulation window: for a 7B model, each accumulated step requires 14GB of FP16 gradient storage, limiting how many steps can accumulate before memory exhaustion. Effective step time increases proportionally with accumulation steps, so accumulating 8 steps means optimizer updates occur 8$\times$ less frequently, potentially slowing convergence despite higher throughput. Most critically, accumulated FP16 gradients risk overflow when summing hundreds of gradient tensors, particularly in early training when loss values are large. A team training a transformer model with 16-step gradient accumulation in FP16 experienced loss spikes and divergence at step 1200; switching to 4-step accumulation with more frequent synchronization resolved the instability despite higher communication costs. Gradient accumulation trades communication for memory and numerical stability. +Engineers use gradient accumulation to simulate larger batch sizes, reducing synchronization frequency from every step to every $K$ steps. The technique appears cost-free since it eliminates $(K-1)/K$ of communication overhead. In production, accumulation introduces memory consumption, latency expansion, and numerical precision risks. Accumulated gradients consume additional memory throughout the accumulation window: for a 7B model, each accumulated step requires 14GB of FP16 gradient storage, limiting how many steps can accumulate before memory exhaustion. Effective step time increases proportionally with accumulation steps, so accumulating 8 steps means optimizer updates occur 8 $\times$ less frequently, potentially slowing convergence despite higher throughput. Most critically, accumulated FP16 gradients risk overflow when summing hundreds of gradient tensors, particularly in early training when loss values are large. A team training a transformer model with 16-step gradient accumulation in FP16 experienced loss spikes and divergence at step 1200; switching to 4-step accumulation with more frequent synchronization resolved the instability despite higher communication costs. Gradient accumulation trades communication for memory and numerical stability. **Pitfall:** ***Using fixed checkpoint intervals regardless of system characteristics.*** diff --git a/book/quarto/contents/vol2/edge_intelligence/edge_intelligence.qmd b/book/quarto/contents/vol2/edge_intelligence/edge_intelligence.qmd index e85a504359..5129309de1 100644 --- a/book/quarto/contents/vol2/edge_intelligence/edge_intelligence.qmd +++ b/book/quarto/contents/vol2/edge_intelligence/edge_intelligence.qmd @@ -30,7 +30,7 @@ A model trained in a datacenter encounters a world it has never seen when deploy ::: {.callout-tip title="Learning Objectives"} - Contrast centralized cloud training, on-device learning, and federated learning by analyzing their data flow, privacy properties, and operational trade-offs. -- Quantify training overhead on edge devices by calculating memory amplification (3-5$\times$), compute costs (2-3$\times$), and energy impacts relative to inference-only deployment. +- Quantify training overhead on edge devices by calculating memory amplification (3-5 $\times$), compute costs (2-3 $\times$), and energy impacts relative to inference-only deployment. - Select model adaptation strategies (weight freezing, LoRA, sparse updates) by comparing memory footprints, expressivity, and convergence for specific device capabilities. - Apply data efficiency techniques (few-shot learning, experience replay, contrastive learning) to enable effective learning from limited local datasets while preventing catastrophic forgetting. - Design federated learning systems that aggregate privacy-preserving updates across heterogeneous devices while managing non-IID data, communication efficiency, and stragglers. @@ -128,7 +128,7 @@ Several alternative approaches often suffice for personalization and adaptation Cloud-based fine-tuning with privacy controls enables personalization through centralized adaptation with appropriate privacy safeguards. User data is processed in batches during off-peak hours using privacy-preserving techniques such as differential privacy[^fn-differential-privacy] or federated analytics. This approach often achieves superior accuracy compared to resource-constrained on-device updates while maintaining acceptable privacy properties for many applications. -[^fn-differential-privacy]: **Differential Privacy**: Mathematical framework that provides quantifiable privacy guarantees by adding carefully calibrated noise to computations. In federated learning, DP ensures that individual user data cannot be inferred from model updates, even by aggregators. Key parameter ε controls privacy-utility tradeoff: smaller ε means stronger privacy but lower model accuracy. Typical deployments use ε=1-8, requiring noise addition that can increase communication overhead by 2-10$\times$ and reduce model accuracy by 1-5%. Essential for regulatory compliance and user trust in distributed learning systems. Differential privacy receives comprehensive treatment in @sec-security-privacy; this overview provides the context needed for federated learning. +[^fn-differential-privacy]: **Differential Privacy**: Mathematical framework that provides quantifiable privacy guarantees by adding carefully calibrated noise to computations. In federated learning, DP ensures that individual user data cannot be inferred from model updates, even by aggregators. Key parameter ε controls privacy-utility tradeoff: smaller ε means stronger privacy but lower model accuracy. Typical deployments use ε=1-8, requiring noise addition that can increase communication overhead by 2-10 $\times$ and reduce model accuracy by 1-5%. Essential for regulatory compliance and user trust in distributed learning systems. Differential privacy receives comprehensive treatment in @sec-security-privacy; this overview provides the context needed for federated learning. User-specific lookup tables combine global models with personalized retrieval mechanisms. The system maintains a lightweight, user-specific lookup table for frequently accessed patterns while using a shared global model for generalization. This hybrid approach provides personalization benefits with minimal computational and storage overhead. @@ -160,7 +160,7 @@ Building on the consumer applications, wearable and health monitoring devices pr Voice interaction technologies present another important application domain with unique acoustic challenges. Wake-word detection[^fn-wake-word-detection] and voice interfaces in devices such as smart speakers and earbuds must recognize voice commands quickly and accurately even in noisy or dynamic acoustic environments. -[^fn-wake-word-detection]: **Wake-Word Detection**: Always-listening keyword spotting that activates voice assistants ("Hey Siri," "OK Google," "Alexa"). These systems run continuously at approximately 1 mW power consumption, roughly 1000$\times$ less than full speech recognition. They use tiny neural networks (approximately 100 KB) with specialized architectures optimized for sub-100 ms latency and minimal false positive rates (<0.1 activations per hour). Modern systems achieve 95%+ accuracy while processing 16 kHz audio in real-time, making on-device personalization (discussed in @sec-edge-intelligence-federated-personalization-3c73) critical for adapting to individual voice characteristics and reducing false activations. +[^fn-wake-word-detection]: **Wake-Word Detection**: Always-listening keyword spotting that activates voice assistants ("Hey Siri," "OK Google," "Alexa"). These systems run continuously at approximately 1 mW power consumption, roughly 1000 $\times$ less than full speech recognition. They use tiny neural networks (approximately 100 KB) with specialized architectures optimized for sub-100 ms latency and minimal false positive rates (<0.1 activations per hour). Modern systems achieve 95%+ accuracy while processing 16 kHz audio in real-time, making on-device personalization (discussed in @sec-edge-intelligence-federated-personalization-3c73) critical for adapting to individual voice characteristics and reducing false activations. These systems face strict latency requirements. Voice interfaces must maintain end-to-end response times under 500 ms to preserve natural conversation flow, with wake-word detection requiring sub-100 ms response times to avoid user frustration. Local training allows models to adapt to the user's unique voice profile and changing ambient context, reducing false positives and missed detections while meeting these demanding performance constraints. This adaptation is particularly valuable in far-field audio settings, where microphone configurations and room acoustics vary dramatically across deployments. @@ -441,7 +441,7 @@ The architectural transformation from centralized to decentralized learning, exa Three efficiency dimensions define the on-device learning design space: algorithmic efficiency (how many parameters must change), compute efficiency (how many operations per update), and data efficiency (how many examples are needed). These dimensions interact multiplicatively rather than additively, creating a constrained optimization problem far more challenging than inference-only deployment. Compression techniques including quantization, pruning, and knowledge distillation (foundations established in model optimization principles) enable deployment on resource-constrained devices, but on-device learning amplifies these constraints dramatically. -On-device learning operates under the same efficiency constraints as inference but with training-specific amplifications that make optimization far more demanding. Where inference requires a single forward pass through the network, training demands forward propagation, gradient computation through backpropagation, and weight updates, increasing memory requirements by 3-5$\times$ and computational costs by 2-3$\times$. The model compression techniques that enable efficient inference become baseline requirements rather than optimizations, as training within edge device constraints would be impossible without aggressive compression. +On-device learning operates under the same efficiency constraints as inference but with training-specific amplifications that make optimization far more demanding. Where inference requires a single forward pass through the network, training demands forward propagation, gradient computation through backpropagation, and weight updates, increasing memory requirements by 3-5 $\times$ and computational costs by 2-3 $\times$. The model compression techniques that enable efficient inference become baseline requirements rather than optimizations, as training within edge device constraints would be impossible without aggressive compression. Given the established motivations for on-device learning, we now examine the fundamental engineering challenges that shape its implementation. Enabling learning on the device requires completely rethinking conventional assumptions about where and how machine learning systems operate. In centralized environments, models are trained with access to extensive compute infrastructure, large and curated datasets, and generous memory and energy budgets. At the edge, none of these assumptions hold, creating a fundamentally different design space. @@ -451,16 +451,16 @@ On-device learning constraints fall into three critical dimensions that define t The transition from inference-only deployment to on-device training creates multiplicative rather than additive complexity. These constraints interact and amplify each other in ways that reshape system design requirements, building on resource optimization principles including quantization, pruning, and knowledge distillation while introducing new challenges specific to distributed learning environments. -Standard efficiency constraints apply to both inference and training. These constraints include quantization, pruning, and knowledge distillation. However, training amplifies each constraint dimension by 3 to 10 times. @tbl-training-amplification quantifies how memory requirements increase 3-5$\times$, compute operations grow 2-3$\times$, and energy consumption can balloon 10-50$\times$ when transitioning from inference to on-device training. +Standard efficiency constraints apply to both inference and training. These constraints include quantization, pruning, and knowledge distillation. However, training amplifies each constraint dimension by 3 to 10 times. @tbl-training-amplification quantifies how memory requirements increase 3-5 $\times$, compute operations grow 2-3 $\times$, and energy consumption can balloon 10-50 $\times$ when transitioning from inference to on-device training. -| **Constraint Dimension** | **Inference** | **Training Amplification** | **Impact on Design** | -|:-------------------------|:--------------------------------------|:--------------------------------------------------------------|:----------------------------------------------------------| -| **Memory Footprint** | Model weights + single activation map | Weights + full activation cache + gradients + optimizer state | 3-5$\times$ increase; forces aggressive compression | -| **Compute Operations** | Forward pass only | Forward + backward + weight update | 2-3$\times$ increase; limits model complexity | -| **Memory Bandwidth** | Sequential weight reads | Bidirectional data flow for gradients | 5-10$\times$ increase; creates bottlenecks | -| **Energy per Sample** | Single inference operation | Multiple gradient steps with convergence | 10-50$\times$ increase; requires opportunistic scheduling | -| **Data Requirements** | Pre-collected, curated datasets | Sparse, noisy, streaming local data | Necessitates sample-efficient methods | -| **Hardware Utilization** | Optimized for forward passes | Different access patterns for backprop | Inference accelerators may not help training | +| **Constraint Dimension** | **Inference** | **Training Amplification** | **Impact on Design** | +|:-------------------------|:--------------------------------------|:--------------------------------------------------------------|:-----------------------------------------------------------| +| **Memory Footprint** | Model weights + single activation map | Weights + full activation cache + gradients + optimizer state | 3-5 $\times$ increase; forces aggressive compression | +| **Compute Operations** | Forward pass only | Forward + backward + weight update | 2-3 $\times$ increase; limits model complexity | +| **Memory Bandwidth** | Sequential weight reads | Bidirectional data flow for gradients | 5-10 $\times$ increase; creates bottlenecks | +| **Energy per Sample** | Single inference operation | Multiple gradient steps with convergence | 10-50 $\times$ increase; requires opportunistic scheduling | +| **Data Requirements** | Pre-collected, curated datasets | Sparse, noisy, streaming local data | Necessitates sample-efficient methods | +| **Hardware Utilization** | Optimized for forward passes | Different access patterns for backprop | Inference accelerators may not help training | : **Training Amplifies Inference Constraints**: On-device learning operates under the same efficiency constraints as inference but with training-specific amplifications that make optimization dramatically more challenging. This table quantifies how each constraint dimension intensifies when transitioning from running pre-trained models to adapting them locally. Amplification factors assume standard backpropagation without optimizations like gradient checkpointing. {#tbl-training-amplification} @@ -671,7 +671,7 @@ The first dimension of on-device learning constraints centers on the model itsel The scale of these constraints becomes apparent when examining specific examples across the device spectrum. The MobileNetV2 architecture, commonly used in mobile vision tasks, requires approximately 14 MB of storage in its standard configuration. While this memory requirement is entirely feasible for modern smartphones with gigabytes of available RAM, it far exceeds the memory available on embedded microcontrollers such as the Arduino Nano 33 BLE Sense[^fn-arduino-constraints], which provides only 256 KB of SRAM and 1 MB of flash storage. This dramatic difference in available resources necessitates aggressive model compression techniques. In such severely constrained platforms, even a single layer of a typical convolutional neural network may exceed available RAM during training due to the need to store intermediate feature maps and gradient information. -[^fn-arduino-constraints]: **Arduino Edge Computing Reality**: The Arduino Nano 33 BLE Sense represents typical microcontroller constraints: 256 KB SRAM is roughly 65,000 times smaller than a modern smartphone's 16 GB RAM (flagship devices). To put this in perspective, storing just one 224$\times$224$\times$3 RGB image (150 KB) would consume 60% of available memory. Training requires 3-5$\times$ more memory for gradients and activations, making even tiny models challenging. The 1 MB flash storage can hold only the smallest quantized models, forcing designers to use 8-bit or even 4-bit representations. +[^fn-arduino-constraints]: **Arduino Edge Computing Reality**: The Arduino Nano 33 BLE Sense represents typical microcontroller constraints: 256 KB SRAM is roughly 65,000 times smaller than a modern smartphone's 16 GB RAM (flagship devices). To put this in perspective, storing just one 224 $\times$ 224 $\times$ 3 RGB image (150 KB) would consume 60% of available memory. Training requires 3-5 $\times$ more memory for gradients and activations, making even tiny models challenging. The 1 MB flash storage can hold only the smallest quantized models, forcing designers to use 8-bit or even 4-bit representations. Beyond static storage requirements, the training process itself dramatically expands the effective memory footprint, creating an additional layer of constraint. Standard backpropagation requires caching activations for each layer during the forward pass, which are then reused during gradient computation in the backward pass. As established in the amplification analysis above, this activation caching multiplies memory requirements compared to inference-only deployment. For a seemingly modest 10-layer convolutional model processing $64 \times 64$ images, the required memory may exceed 1 to 2 MB, well beyond the SRAM capacity of most embedded systems and highlighting the fundamental tension between model expressiveness and resource availability. @@ -681,9 +681,9 @@ The practical implications of battery and thermal constraints extend beyond just Given these multifaceted constraints, the model architecture itself must be fundamentally designed with on-device learning capabilities in mind from the outset. Many conventional architectures, such as large transformers or deep convolutional networks, are simply not viable for on-device adaptation due to their inherent size and computational complexity. Instead, specialized lightweight architectures such as MobileNets[^fn-mobilenet-innovation], SqueezeNet [@iandola2016squeezenet], and EfficientNet [@tan2019efficientnet] have been developed specifically for resource-constrained environments. These architectures leverage efficiency principles and architectural optimizations, rethinking how neural networks can be structured. These specialized models employ techniques such as depthwise separable convolutions[^fn-depthwise-separable], bottleneck layers, and aggressive quantization to dramatically reduce memory and compute requirements while maintaining sufficient performance for practical applications. -[^fn-mobilenet-innovation]: **MobileNet Innovation**: Google's MobileNet family revolutionized mobile AI by achieving 10–20$\times$ parameter reduction compared to traditional CNNs. MobileNetV1 (2017) used depthwise separable convolutions to reduce floating-point operations (FLOPs) by 8–9$\times$, while MobileNetV2 (2018) added inverted residuals and linear bottlenecks. The breakthrough allowed real-time inference on smartphones: MobileNetV2 runs ImageNet classification in approximately 75 ms on a Pixel phone versus 1.8 seconds for ResNet-50 [@he2016deep]. +[^fn-mobilenet-innovation]: **MobileNet Innovation**: Google's MobileNet family revolutionized mobile AI by achieving 10–20 $\times$ parameter reduction compared to traditional CNNs. MobileNetV1 (2017) used depthwise separable convolutions to reduce floating-point operations (FLOPs) by 8–9 $\times$, while MobileNetV2 (2018) added inverted residuals and linear bottlenecks. The breakthrough allowed real-time inference on smartphones: MobileNetV2 runs ImageNet classification in approximately 75 ms on a Pixel phone versus 1.8 seconds for ResNet-50 [@he2016deep]. -[^fn-depthwise-separable]: **Depthwise Separable Convolutions**: First popularized in the Xception architecture [@chollet2017xception], this technique decomposes standard convolution into two operations: depthwise convolution (applies single filter per input channel) and pointwise convolution (1$\times$1 convolution to combine channels). For a 3$\times$3 convolution with 512 input/output channels, standard convolution requires 2.4 M parameters while depthwise separable needs only 13.8 K, a 174$\times$ reduction. The computational savings are similarly dramatic, making real-time inference possible on mobile CPUs. +[^fn-depthwise-separable]: **Depthwise Separable Convolutions**: First popularized in the Xception architecture [@chollet2017xception], this technique decomposes standard convolution into two operations: depthwise convolution (applies single filter per input channel) and pointwise convolution (1 $\times$ 1 convolution to combine channels). For a 3 $\times$ 3 convolution with 512 input/output channels, standard convolution requires 2.4 M parameters while depthwise separable needs only 13.8 K, a 174 $\times$ reduction. The computational savings are similarly dramatic, making real-time inference possible on mobile CPUs. These architectures are often designed to be modular, allowing for easy adaptation and fine-tuning. For example, MobileNets [@howard2017mobilenets] and MobileNetV2 [@sandler2018mobilenetv2] can be configured with different width multipliers and resolution settings to balance performance and resource usage. Concretely, MobileNetV2 with α=1.0 requires 3.4 M parameters (13.6 MB in FP32), but with α=0.5 this drops to 0.7 M parameters (2.8 MB), enabling deployment on devices with just 4 MB available RAM. This flexibility is important for on-device learning, where the model must adapt to the specific constraints of the deployment environment. @@ -711,9 +711,9 @@ Building on this edge hardware landscape, from microcontrollers to mobile AI acc The key difference: backpropagation requires significantly higher memory bandwidth than inference due to gradient computation and activation caching, weight updates create write-heavy access patterns unlike inference's read-only operations, and optimizer state management demands additional memory allocation that inference never encounters. These training-specific demands mean hardware perfectly adequate for inference may prove entirely inadequate for adaptation, even when updating only a small parameter subset. -At the most constrained end of the spectrum, devices such as the STM32F4[^fn-stm32-constraints] or ESP32[^fn-esp32-capabilities] microcontrollers offer only a few hundred kilobytes of SRAM and completely lack hardware support for floating-point operations [@lai2020tinyml]. Libraries like CMSIS-NN [@lai2018cmsis] provide optimized neural network kernels specifically designed for Arm Cortex-M processors, achieving 4.6$\times$ runtime improvement over baseline implementations through fixed-point arithmetic and SIMD optimizations. These extreme constraints represent the fundamental limitations of edge hardware. Such severe limitations preclude the use of conventional deep learning libraries and require models to be meticulously designed for integer arithmetic and minimal runtime memory allocation. In these environments, even apparently simple models require highly specialized techniques, including quantization-aware training[^fn-quantization-aware] and selective parameter updates, to execute training loops without exceeding memory or power budgets. +At the most constrained end of the spectrum, devices such as the STM32F4[^fn-stm32-constraints] or ESP32[^fn-esp32-capabilities] microcontrollers offer only a few hundred kilobytes of SRAM and completely lack hardware support for floating-point operations [@lai2020tinyml]. Libraries like CMSIS-NN [@lai2018cmsis] provide optimized neural network kernels specifically designed for Arm Cortex-M processors, achieving 4.6 $\times$ runtime improvement over baseline implementations through fixed-point arithmetic and SIMD optimizations. These extreme constraints represent the fundamental limitations of edge hardware. Such severe limitations preclude the use of conventional deep learning libraries and require models to be meticulously designed for integer arithmetic and minimal runtime memory allocation. In these environments, even apparently simple models require highly specialized techniques, including quantization-aware training[^fn-quantization-aware] and selective parameter updates, to execute training loops without exceeding memory or power budgets. -[^fn-stm32-constraints]: **STM32F4 Microcontroller Reality**: The STM32F4 represents the harsh reality of embedded computing: 192 KB SRAM (roughly the size of a small JPEG image) and 1 MB flash storage, running at 168 MHz without floating-point hardware acceleration. Integer arithmetic is 10–100$\times$ slower than dedicated floating-point units found in mobile chips. Power consumption is approximately 100 mW during active processing, requiring careful duty-cycling to preserve battery life. These constraints make even simple neural networks challenging: a 10-neuron hidden layer requires approximately 40 KB for weights alone in FP32. +[^fn-stm32-constraints]: **STM32F4 Microcontroller Reality**: The STM32F4 represents the harsh reality of embedded computing: 192 KB SRAM (roughly the size of a small JPEG image) and 1 MB flash storage, running at 168 MHz without floating-point hardware acceleration. Integer arithmetic is 10–100 $\times$ slower than dedicated floating-point units found in mobile chips. Power consumption is approximately 100 mW during active processing, requiring careful duty-cycling to preserve battery life. These constraints make even simple neural networks challenging: a 10-neuron hidden layer requires approximately 40 KB for weights alone in FP32. [^fn-esp32-capabilities]: **ESP32 Edge Computing**: The ESP32 provides 520 KB SRAM and dual-core processing at 240 MHz, making it more capable than STM32F4 but still severely constrained. Its key advantage is built-in WiFi and Bluetooth for federated learning scenarios. However, the lack of hardware floating-point support means all ML operations must use integer quantization. Real-world deployments show 8-bit quantized models can achieve 95% of FP32 accuracy while fitting in approximately 50 KB memory, enabling basic on-device training for simple tasks like sensor anomaly detection. @@ -725,19 +725,19 @@ The practical implications are stark: while the STM32F4 microcontroller can run Moving up the computational hierarchy, mobile-class hardware represents improvement but still operates under severe constraints. Platforms including the Qualcomm Snapdragon, Apple Neural Engine[^fn-ondevice-neural-engine], and Google Tensor SoC[^fn-tensor-soc] provide significantly more compute power than microcontrollers, often featuring dedicated AI accelerators and optimized support for 8-bit or mixed-precision[^fn-mixed-precision] matrix operations. These accelerators offer dedicated matrix multiplication units, on-chip memory hierarchies, and power management features specifically designed for neural network inference and, increasingly, training workloads. While these platforms can support more sophisticated training routines, including full backpropagation over compact models, they still fall far short of the computational throughput and memory bandwidth available in centralized data centers. For instance, training a lightweight transformer[^fn-transformer-mobile] on a smartphone is technically feasible but must be tightly bounded in both time and energy consumption to avoid degrading the user experience, highlighting the persistent tension between learning capabilities and practical deployment constraints. -[^fn-ondevice-neural-engine]: **Apple Neural Engine Evolution**: Apple's Neural Engine has evolved dramatically since the A11 Bionic. The A17 Pro (2023) features a 16-core Neural Engine delivering `{python} mobile_npu_tops` TOPS, roughly equivalent to an NVIDIA GTX 1080 Ti. This represents a 58$\times$ improvement over the original A11. The Neural Engine specializes in matrix operations with dedicated 8-bit and 16-bit arithmetic units, enabling efficient on-device training. Real-world performance: fine-tuning a MobileNet classifier takes approximately 2 seconds versus 45 seconds on CPU alone, while consuming only approximately 500 mW additional power. +[^fn-ondevice-neural-engine]: **Apple Neural Engine Evolution**: Apple's Neural Engine has evolved dramatically since the A11 Bionic. The A17 Pro (2023) features a 16-core Neural Engine delivering `{python} mobile_npu_tops` TOPS, roughly equivalent to an NVIDIA GTX 1080 Ti. This represents a 58 $\times$ improvement over the original A11. The Neural Engine specializes in matrix operations with dedicated 8-bit and 16-bit arithmetic units, enabling efficient on-device training. Real-world performance: fine-tuning a MobileNet classifier takes approximately 2 seconds versus 45 seconds on CPU alone, while consuming only approximately 500 mW additional power. [^fn-tensor-soc]: **Google Tensor SoC Architecture**: Google's Tensor chips (starting with Pixel 6 in 2021) feature a custom TPU v1-derived Edge TPU optimized for ML workloads. Unlike Apple's Neural Engine, Tensor optimizes for Google's specific models (speech recognition, computational photography). The TPU provides efficient 8-bit integer operations while consuming only 2 W, making it highly efficient for federated learning scenarios where devices train locally on speech or image data. [^fn-mixed-precision]: **Mixed-Precision Training**: Uses different numerical precisions for different operations, typically FP16 for forward/backward passes and FP32 for parameter updates. This halves memory usage and doubles throughput on modern hardware with Tensor Cores, while maintaining training stability through automatic loss scaling. Mobile implementations often use INT8 for inference and FP16 for gradient computation, balancing accuracy with hardware constraints. -[^fn-transformer-mobile]: **Lightweight Transformers**: Mobile-optimized transformer architectures like MobileBERT [@sun2020mobilebert] and DistilBERT [@sanh2019distilbert] achieve 4–6$\times$ speedup over full models through techniques like knowledge distillation, layer reduction, and attention head pruning. MobileBERT retains 97% of BERT-base accuracy while running inference in approximately 40 ms on mobile CPUs versus 160 ms for full BERT. Key optimizations include bottleneck attention mechanisms and specialized mobile-friendly layer configurations. +[^fn-transformer-mobile]: **Lightweight Transformers**: Mobile-optimized transformer architectures like MobileBERT [@sun2020mobilebert] and DistilBERT [@sanh2019distilbert] achieve 4–6 $\times$ speedup over full models through techniques like knowledge distillation, layer reduction, and attention head pruning. MobileBERT retains 97% of BERT-base accuracy while running inference in approximately 40 ms on mobile CPUs versus 160 ms for full BERT. Key optimizations include bottleneck attention mechanisms and specialized mobile-friendly layer configurations. These computational limitations become especially acute in real-time or battery-operated systems, as demonstrated in camera processing requirements, where specific latency budgets create hard architectural constraints. Camera applications processing at 30 FPS cannot exceed 33 ms per frame, voice interfaces require rapid response times for natural interaction, AR/VR systems demand sub-20 ms motion-to-photon latency to prevent user discomfort, and safety-critical control systems must respond within 10 ms to ensure operational safety. These quantitative constraints determine whether on-device learning is feasible or whether cloud-based alternatives become architecturally necessary. In a smartphone-based speech recognizer, on-device adaptation must seamlessly coexist with primary inference workloads without interfering with response latency or system responsiveness. Similarly, in wearable medical monitors, training must occur opportunistically during carefully managed windows (typically during periods of low activity or charging) to preserve battery life and avoid thermal management issues. -Beyond raw computational capacity, the architectural implications of these hardware constraints extend into fundamental system design choices. Training operations exhibit fundamentally different memory access patterns than inference workloads: backpropagation requires 3–5$\times$ higher memory bandwidth due to gradient computation and activation caching, creating bottlenecks that pure computational metrics don't capture. Modern edge accelerators attempt to address these challenges through increasingly specialized hardware features. Adaptive precision datapaths allow dynamic switching between INT4 for forward passes and FP16 for gradient computation, optimizing both accuracy and efficiency within power budgets. Sparse computation units accelerate selective parameter updates by skipping zero gradients, a capability critical for efficient bias-only and LoRA adaptations. Near-memory compute architectures[^fn-near-memory-compute] reduce data movement costs by performing gradient updates directly adjacent to weight storage, addressing the memory bandwidth bottleneck. However, most current edge accelerators remain fundamentally optimized for inference workloads, creating significant hardware-software co-design opportunities for future generations of on-device training accelerators specifically designed to handle the unique demands of local adaptation. +Beyond raw computational capacity, the architectural implications of these hardware constraints extend into fundamental system design choices. Training operations exhibit fundamentally different memory access patterns than inference workloads: backpropagation requires 3–5 $\times$ higher memory bandwidth due to gradient computation and activation caching, creating bottlenecks that pure computational metrics don't capture. Modern edge accelerators attempt to address these challenges through increasingly specialized hardware features. Adaptive precision datapaths allow dynamic switching between INT4 for forward passes and FP16 for gradient computation, optimizing both accuracy and efficiency within power budgets. Sparse computation units accelerate selective parameter updates by skipping zero gradients, a capability critical for efficient bias-only and LoRA adaptations. Near-memory compute architectures[^fn-near-memory-compute] reduce data movement costs by performing gradient updates directly adjacent to weight storage, addressing the memory bandwidth bottleneck. However, most current edge accelerators remain fundamentally optimized for inference workloads, creating significant hardware-software co-design opportunities for future generations of on-device training accelerators specifically designed to handle the unique demands of local adaptation. -[^fn-near-memory-compute]: **Near-Memory Computing**: Places processing units directly adjacent to or within memory arrays, dramatically reducing data movement costs. Traditional von Neumann architectures spend 100-1000$\times$ more energy moving data than computing on it. Near-memory designs can perform matrix operations with 10-100$\times$ better energy efficiency by eliminating costly memory bus transfers. Critical for edge training where gradient computations require intensive memory access patterns that overwhelm traditional cache hierarchies. +[^fn-near-memory-compute]: **Near-Memory Computing**: Places processing units directly adjacent to or within memory arrays, dramatically reducing data movement costs. Traditional von Neumann architectures spend 100-1000 $\times$ more energy moving data than computing on it. Near-memory designs can perform matrix operations with 10-100 $\times$ better energy efficiency by eliminating costly memory bus transfers. Critical for edge training where gradient computations require intensive memory access patterns that overwhelm traditional cache hierarchies. #### The Mobile Memory Wall {#sec-edge-intelligence-mobile-memory-wall-5ffe} @@ -748,7 +748,7 @@ The quantitative disparity is stark: - **Datacenter (H100)**: HBM3 provides `{python} h100_mem_bw_gbs` GB/s bandwidth. - **Flagship Mobile (A17/Snapdragon 8 Gen 3)**: LPDDR5X provides 64–100 GB/s bandwidth. -This 30–50$\times$ bandwidth gap means that even if a model fits in mobile RAM, it will generate tokens 30–50$\times$ slower than a datacenter GPU. Consequently, on-device LLM serving requires aggressive quantization (INT4 or even INT2) not merely for capacity, but as a **bandwidth survival strategy**. By reducing model size by $8\times$, we effectively increase the relative bandwidth, making interactive generation speeds possible on mobile hardware. +This 30–50 $\times$ bandwidth gap means that even if a model fits in mobile RAM, it will generate tokens 30–50 $\times$ slower than a datacenter GPU. Consequently, on-device LLM serving requires aggressive quantization (INT4 or even INT2) not merely for capacity, but as a **bandwidth survival strategy**. By reducing model size by $8\times$, we effectively increase the relative bandwidth, making interactive generation speeds possible on mobile hardware. ### Edge Hardware Integration Challenges {#sec-edge-intelligence-edge-hardware-integration-challenges-a240} @@ -771,7 +771,7 @@ Energy and thermal management represent perhaps the most challenging aspect of o * Total per token: $6 \text{ nJ/param} \times 10^9 \text{ params} = 6 \text{ Joules/token}$. 4. **Capacity**: $2,700 \text{ J} / 6 \text{ J/token} = \mathbf{450 \text{ tokens}}$. -**The Systems Conclusion**: With a 1B parameter model, 5% of a phone battery buys you only **450 tokens** of training (a few paragraphs). Full fine-tuning is impossible. You *must* use **PEFT (Parameter-Efficient Fine-Tuning)** or update only the last layer to reduce the energy cost per token by 100$\times$. +**The Systems Conclusion**: With a 1B parameter model, 5% of a phone battery buys you only **450 tokens** of training (a few paragraphs). Full fine-tuning is impossible. You *must* use **PEFT (Parameter-Efficient Fine-Tuning)** or update only the last layer to reduce the energy cost per token by 100 $\times$. ::: The mobile power budget hierarchy reveals the tight constraints under which on-device learning must operate. Smartphone sustained processing is limited to 2–3 W to prevent user-noticeable heating and maintain acceptable battery life throughout the day. Peak training burst mode can reach 10 W, but this power level is sustainable for only 10–30 seconds before thermal throttling kicks in to protect the hardware. Dedicated neural processing units consume 0.5–2 W for AI workloads, offering optimized power efficiency compared to general-purpose processors. CPU-based AI processing requires 3–5 W and demands aggressive thermal management with duty cycling to prevent overheating, making it the least power-efficient option for sustained on-device learning. @@ -793,7 +793,7 @@ The fundamental physics of energy consumption reveals why local processing is al **Conclusion**: If you can extract insight from data using fewer than ~100k operations per bit, **local processing is strictly more energy efficient** than cloud offloading. This ratio drives the architecture of Federated Learning: compute is cheap; radio transmission is expensive. ::: -The power consumption characteristics of training workloads create additional layers of constraint that extend beyond simple computational capacity. Power consumption scales superlinearly with model size and training complexity, with training operations consuming 10-50$\times$ more power than equivalent inference workloads due to the substantial computational overhead of gradient computation (consuming 40-70% of training power), weight updates (20-30%), and dramatically increased data movement between memory hierarchies (10-30%). To maintain acceptable user experience, mobile devices typically budget only 500-1000 mW for sustained ML training, effectively limiting practical training sessions to 10-100 minutes daily under normal usage patterns. This severe power constraint fundamentally shifts the design priority from maximizing computational throughput to optimizing power efficiency, requiring careful co-optimization of algorithms and hardware utilization patterns. +The power consumption characteristics of training workloads create additional layers of constraint that extend beyond simple computational capacity. Power consumption scales superlinearly with model size and training complexity, with training operations consuming 10-50 $\times$ more power than equivalent inference workloads due to the substantial computational overhead of gradient computation (consuming 40-70% of training power), weight updates (20-30%), and dramatically increased data movement between memory hierarchies (10-30%). To maintain acceptable user experience, mobile devices typically budget only 500-1000 mW for sustained ML training, effectively limiting practical training sessions to 10-100 minutes daily under normal usage patterns. This severe power constraint fundamentally shifts the design priority from maximizing computational throughput to optimizing power efficiency, requiring careful co-optimization of algorithms and hardware utilization patterns. The thermal management challenges extend far beyond simple power limits, creating complex dynamic constraints that vary with environmental conditions and usage patterns. Training workloads generate localized heat that can trigger protective throttling in specific processor cores or accelerator units, often in unpredictable ways that depend on ambient temperature and device design. Modern mobile SoCs implement sophisticated thermal management systems, including dynamic voltage and frequency scaling (DVFS)[^fn-dvfs-mobile], core migration between efficiency and performance clusters, and selective shutdown of non-essential processing units. Successfully deployed on-device learning systems must intimately integrate with these thermal management frameworks, intelligently scheduling training bursts during optimal thermal windows and gracefully degrading performance when thermal limits are approached, rather than simply failing or causing user-visible performance problems. @@ -807,7 +807,7 @@ The device memory hierarchy spans several orders of magnitude across different d The memory expansion during training creates particularly acute challenges that often determine system feasibility. Standard backpropagation requires caching intermediate activations for each layer during the forward pass, which are then reused during gradient computation in the backward pass, creating substantial memory overhead. A MobileNetV2 model requiring just 14 MB for inference balloons to 50-70 MB during training, often exceeding the available memory budget on many mobile devices and making training impossible without aggressive optimization. This dramatic expansion necessitates sophisticated model compression techniques that must compound multiplicatively: INT8 quantization provides $4\times$ memory reduction, structured pruning achieves $10\times$ parameter reduction, and knowledge distillation enables $5\times$ model size reduction while maintaining accuracy within 2-5% of the original model. These techniques must be carefully combined to achieve the aggressive compression ratios required for practical deployment. -Given these memory constraints, cache optimization becomes absolutely critical for achieving acceptable performance with constrained memory pools. Modern mobile SoCs feature complex memory hierarchies with L1 cache (32-64 KB), L2 cache (1-8 MB), and system memory (4-16 GB) that exhibit 10-100$\times$ latency differences between levels, creating severe performance cliffs when working sets exceed cache capacity. Training workloads that exceed cache capacity face dramatic performance degradation due to memory bandwidth bottlenecks that can slow training by orders of magnitude. Successful on-device learning systems must carefully design data access patterns to maximize cache hit rates, often requiring specialized memory layouts that group related parameters for spatial locality, carefully sized mini-batches that fit entirely within cache constraints, and sophisticated gradient accumulation strategies that minimize expensive memory bus traffic. +Given these memory constraints, cache optimization becomes absolutely critical for achieving acceptable performance with constrained memory pools. Modern mobile SoCs feature complex memory hierarchies with L1 cache (32-64 KB), L2 cache (1-8 MB), and system memory (4-16 GB) that exhibit 10-100 $\times$ latency differences between levels, creating severe performance cliffs when working sets exceed cache capacity. Training workloads that exceed cache capacity face dramatic performance degradation due to memory bandwidth bottlenecks that can slow training by orders of magnitude. Successful on-device learning systems must carefully design data access patterns to maximize cache hit rates, often requiring specialized memory layouts that group related parameters for spatial locality, carefully sized mini-batches that fit entirely within cache constraints, and sophisticated gradient accumulation strategies that minimize expensive memory bus traffic. The memory bandwidth limitations become particularly acute during training. While inference workloads primarily read model weights sequentially, training requires bidirectional data flow for gradient computation and weight updates. This increased memory traffic can saturate the memory subsystem, creating bottlenecks that limit training throughput regardless of computational capacity. Advanced implementations employ techniques such as gradient checkpointing[^fn-gradient-checkpointing] to trade computation for memory, and mixed-precision training to reduce bandwidth requirements while maintaining numerical stability. @@ -817,7 +817,7 @@ The memory bandwidth limitations become particularly acute during training. Whil Different mobile platforms provide distinct acceleration capabilities that determine not only achievable model complexity but also feasible learning paradigms. The architectural differences between these accelerators fundamentally shape the design space for on-device training algorithms, influencing everything from numerical precision choices to gradient computation strategies. -Current generation mobile accelerators demonstrate remarkable diversity in their capabilities and optimization focus. Apple's Neural Engine in the A17 Pro delivers `{python} mobile_npu_tops` TOPS peak performance specialized for 8-bit and 16-bit operations, optimized primarily for CoreML inference patterns with limited training support, making it ideal for inference-heavy adaptation techniques. Qualcomm's Hexagon DSP in the Snapdragon 8 Gen 3 achieves 45 TOPS with flexible precision support and programmable vector units, enabling mixed-precision training workflows that can adapt precision dynamically based on training phase and memory constraints. Google's Tensor TPU in the Pixel 8 is optimized specifically for TensorFlow Lite operations with strong INT8 performance and tight integration with federated learning frameworks, reflecting Google's strategic focus on distributed learning scenarios. The energy efficiency comparison reveals why dedicated neural processing units are essential: NPUs achieve 1-5 TOPS per watt versus general-purpose CPUs at just 0.1-0.2 TOPS per watt, representing a 5-50$\times$ efficiency advantage that makes the difference between feasible and infeasible on-device training. +Current generation mobile accelerators demonstrate remarkable diversity in their capabilities and optimization focus. Apple's Neural Engine in the A17 Pro delivers `{python} mobile_npu_tops` TOPS peak performance specialized for 8-bit and 16-bit operations, optimized primarily for CoreML inference patterns with limited training support, making it ideal for inference-heavy adaptation techniques. Qualcomm's Hexagon DSP in the Snapdragon 8 Gen 3 achieves 45 TOPS with flexible precision support and programmable vector units, enabling mixed-precision training workflows that can adapt precision dynamically based on training phase and memory constraints. Google's Tensor TPU in the Pixel 8 is optimized specifically for TensorFlow Lite operations with strong INT8 performance and tight integration with federated learning frameworks, reflecting Google's strategic focus on distributed learning scenarios. The energy efficiency comparison reveals why dedicated neural processing units are essential: NPUs achieve 1-5 TOPS per watt versus general-purpose CPUs at just 0.1-0.2 TOPS per watt, representing a 5-50 $\times$ efficiency advantage that makes the difference between feasible and infeasible on-device training. These accelerators determine not just raw performance but feasible learning paradigms and algorithmic approaches. Apple's Neural Engine excels at fixed-precision inference workloads but provides limited support for the dynamic precision requirements of gradient computation, making it more suitable for inference-heavy adaptation techniques like few-shot learning. Qualcomm's Hexagon DSP offers greater training flexibility through its programmable vector units and support for mixed-precision arithmetic, enabling more sophisticated on-device training including full backpropagation on compact models. Google's Tensor TPU integrates tightly with federated learning frameworks and provides optimized communication primitives for distributed training scenarios. @@ -829,7 +829,7 @@ However, most current edge accelerators remain primarily optimized for inference The constraint analysis above reveals three fundamental challenge categories that define the on-device learning design space. Each constraint category directly drives a corresponding solution pillar, creating a systematic engineering approach to this complex systems problem. The constraint-to-solution mapping follows naturally from understanding how specific limitations necessitate particular technical responses. -The resource amplification effects, where training increases memory requirements by 3–10$\times$, computational costs by 2–3$\times$, and energy consumption proportionally, directly necessitate Model Adaptation approaches. When traditional training becomes impossible due to resource constraints, systems must fundamentally reduce the scope of parameter updates while preserving learning capability. +The resource amplification effects, where training increases memory requirements by 3–10 $\times$, computational costs by 2–3 $\times$, and energy consumption proportionally, directly necessitate Model Adaptation approaches. When traditional training becomes impossible due to resource constraints, systems must fundamentally reduce the scope of parameter updates while preserving learning capability. The information scarcity constraints, including limited local datasets, non-IID distributions, privacy restrictions on data sharing, and minimal supervision, directly drive Data Efficiency solutions. When conventional data-hungry approaches fail due to insufficient local information, systems must extract maximum learning signal from minimal examples. @@ -837,12 +837,12 @@ The coordination challenges, such as device heterogeneity, intermittent connecti @tbl-constraint-solution-mapping reveals how this constraint-to-solution mapping creates a systematic engineering framework: Model Adaptation addresses memory and compute limits through selective parameter updates, Data Efficiency maximizes learning from scarce private samples, and Federated Coordination enables privacy-preserving collaboration. Rather than viewing these as independent techniques, successful systems orchestrate all three approaches to create coherent adaptive systems that operate effectively within edge constraints. -| **Constraint Category** | **Key Challenges** | **Solution Approach** | -|:---------------------------|:-------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------| -| **Resource Amplification** | • Training workloads (3-10$\times$ memory) • Memory limitations • Power constraints | **Model Adaptation** • Parameter-efficient updates • Selective layer fine-tuning • Low-rank adaptations | -| **Information Scarcity** | • Limited local datasets • Non-IID distributions | **Data Efficiency** | -| | • Privacy restrictions | • Few-shot learning | -| | | • Meta-learning • Transfer learning | +| **Constraint Category** | **Key Challenges** | **Solution Approach** | +|:---------------------------|:--------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------| +| **Resource Amplification** | • Training workloads (3-10 $\times$ memory) • Memory limitations • Power constraints | **Model Adaptation** • Parameter-efficient updates • Selective layer fine-tuning • Low-rank adaptations | +| **Information Scarcity** | • Limited local datasets • Non-IID distributions | **Data Efficiency** | +| | • Privacy restrictions | • Few-shot learning | +| | | • Meta-learning • Transfer learning | : **Constraint-Solution Mapping**: The three fundamental constraint categories in on-device learning each drive corresponding solution approaches through direct necessity. {#tbl-constraint-solution-mapping} @@ -850,7 +850,7 @@ The subsequent sections examine each solution pillar systematically, building on ## Model Adaptation {#sec-edge-intelligence-model-adaptation-6a82} -The computational and memory constraints outlined above create seemingly impossible challenges for model training. Yet these constraints also reveal solution pathways when approached systematically. Model adaptation, the first pillar in our three-pillar framework (@tbl-constraint-solution-mapping), addresses resource amplification by reducing the scope of parameter updates. Where full training requires 3-5$\times$ inference memory, strategic adaptation can achieve meaningful personalization within inference-comparable budgets. This pillar answers a fundamental question: which parameters must change to capture local patterns, and which can remain frozen? +The computational and memory constraints outlined above create seemingly impossible challenges for model training. Yet these constraints also reveal solution pathways when approached systematically. Model adaptation, the first pillar in our three-pillar framework (@tbl-constraint-solution-mapping), addresses resource amplification by reducing the scope of parameter updates. Where full training requires 3-5 $\times$ inference memory, strategic adaptation can achieve meaningful personalization within inference-comparable budgets. This pillar answers a fundamental question: which parameters must change to capture local patterns, and which can remain frozen? The engineering challenge centers on navigating a fundamental trade-off space: adaptation expressivity versus resource consumption. At one extreme, updating all parameters provides maximum flexibility but exceeds edge device capabilities. At the other extreme, no adaptation preserves resources but fails to capture user-specific patterns. Effective on-device learning systems must operate in the middle ground, selecting adaptation strategies based on three key engineering criteria. @@ -866,7 +866,7 @@ These approaches build on established architectural principles while strategical ### Weight Freezing {#sec-edge-intelligence-weight-freezing-3407} -The most straightforward approach to making on-device learning feasible is to dramatically reduce the number of parameters that require updating. One of the simplest and most effective strategies for achieving this reduction is to freeze the majority of a model's parameters and adapt only a carefully chosen minimal subset. The most widely used approach within this family is bias-only adaptation, in which all weights are held fixed and only the bias terms (typically scalar offsets applied after linear or convolutional layers) are updated during training. This simple constraint creates significant benefits: it reduces the number of trainable parameters (often by 100-1000$\times$), simplifies memory management during backpropagation, and helps mitigate overfitting when training data is sparse or noisy. +The most straightforward approach to making on-device learning feasible is to dramatically reduce the number of parameters that require updating. One of the simplest and most effective strategies for achieving this reduction is to freeze the majority of a model's parameters and adapt only a carefully chosen minimal subset. The most widely used approach within this family is bias-only adaptation, in which all weights are held fixed and only the bias terms (typically scalar offsets applied after linear or convolutional layers) are updated during training. This simple constraint creates significant benefits: it reduces the number of trainable parameters (often by 100-1000 $\times$), simplifies memory management during backpropagation, and helps mitigate overfitting when training data is sparse or noisy. Consider a standard neural network layer: $$ @@ -1212,9 +1212,9 @@ In contrast, the TinyTL architecture freezes all weights and updates only the bi Because the base model remains unchanged, TinyTL assumes that the pretrained features are sufficiently expressive for downstream tasks. The bias terms allow for minor but meaningful shifts in model behavior, particularly for personalization tasks. When domain shift is more significant, TinyTL can optionally incorporate small residual adapters to improve expressivity, all while preserving the system's tight memory and energy profile. -These design choices allow TinyTL to reduce training memory usage by 10$\times$. For instance, adapting a MobileNetV2 model using TinyTL can reduce the number of updated parameters from over 3 million to fewer than 50,000[^fn-tinytl-efficiency]. Combined with quantization, this allows local adaptation on devices with only a few hundred kilobytes of memory, making on-device learning truly feasible in constrained environments. +These design choices allow TinyTL to reduce training memory usage by 10 $\times$. For instance, adapting a MobileNetV2 model using TinyTL can reduce the number of updated parameters from over 3 million to fewer than 50,000[^fn-tinytl-efficiency]. Combined with quantization, this allows local adaptation on devices with only a few hundred kilobytes of memory, making on-device learning truly feasible in constrained environments. -[^fn-tinytl-efficiency]: **TinyTL Memory Breakthrough**: TinyTL's 60$\times$ parameter reduction (3.4 M to 50 K) translates to dramatic memory savings. In FP32, MobileNetV2 requires approximately 12 MB for weights plus approximately 8 MB for activation caching during training, exceeding most microcontroller capabilities. TinyTL reduces this to approximately 200 KB weights plus approximately 400 KB activations, fitting comfortably within a 1 MB memory budget. Real deployments on STM32H7 achieve 85% of full fine-tuning accuracy while using 15$\times$ less memory and completing updates in approximately 30 seconds versus 8 minutes for full training. +[^fn-tinytl-efficiency]: **TinyTL Memory Breakthrough**: TinyTL's 60 $\times$ parameter reduction (3.4 M to 50 K) translates to dramatic memory savings. In FP32, MobileNetV2 requires approximately 12 MB for weights plus approximately 8 MB for activation caching during training, exceeding most microcontroller capabilities. TinyTL reduces this to approximately 200 KB weights plus approximately 400 KB activations, fitting comfortably within a 1 MB memory budget. Real deployments on STM32H7 achieve 85% of full fine-tuning accuracy while using 15 $\times$ less memory and completing updates in approximately 30 seconds versus 8 minutes for full training. ### Structured Parameter Updates {#sec-edge-intelligence-structured-parameter-updates-f1a1} @@ -1255,7 +1255,7 @@ $$ This formulation is commonly used in LoRA (Low-Rank Adaptation)[^fn-lora] techniques, originally developed for transformer models [@hu2021lora] but broadly applicable across architectures. From a systems engineering perspective, LoRA addresses critical connectivity and resource trade-offs in on-device learning deployment. -[^fn-lora]: **LoRA (Low-Rank Adaptation)**: Introduced by Microsoft in 2021, LoRA enables efficient fine-tuning by learning low-rank decomposition matrices rather than updating full weight matrices. For a weight matrix W, LoRA learns rank-r matrices A and B such that the update is BA (where r << original dimensions). This reduces trainable parameters by 100–10,000$\times$ while maintaining 90–95% adaptation quality. LoRA has become the standard for parameter-efficient fine-tuning in large language models. +[^fn-lora]: **LoRA (Low-Rank Adaptation)**: Introduced by Microsoft in 2021, LoRA enables efficient fine-tuning by learning low-rank decomposition matrices rather than updating full weight matrices. For a weight matrix W, LoRA learns rank-r matrices A and B such that the update is BA (where r << original dimensions). This reduces trainable parameters by 100–10,000 $\times$ while maintaining 90–95% adaptation quality. LoRA has become the standard for parameter-efficient fine-tuning in large language models. Consider a mobile deployment where a 7B parameter language model requires 14 GB for full fine-tuning, which is impossible on typical smartphones with 6–8 GB total memory. LoRA with rank-16 reduces this to approximately 100 MB of trainable parameters (0.7% of original), enabling local adaptation within mobile memory constraints. @@ -1825,7 +1825,7 @@ At the heart of federated learning is a coordination mechanism that allows many The most widely used baseline for this process is Federated Averaging (FedAvg)[^fn-fedavg], which has become a canonical algorithm for federated learning [@mcmahan2017communication]. In FedAvg, each device trains its local copy of the model using stochastic gradient descent (SGD) on its private data. -[^fn-fedavg]: **Federated Averaging (FedAvg)**: Introduced by Google in 2017, FedAvg revolutionized distributed ML by averaging model weights rather than gradients. Each client performs multiple local SGD steps (typically 1-20) before sending weights to the server, reducing communication by 10-100$\times$ compared to distributed SGD. The key insight: local updates contain richer information than single gradients, enabling convergence with far fewer communication rounds. FedAvg powers production systems like Gboard, processing billions of devices. After a fixed number of local steps, each device sends its updated model parameters to the server. The server computes a weighted average of these parameters, which are weighted according to the number of data samples on each device, and updates the global model accordingly. This updated model is then sent back to the devices, completing one round of training. +[^fn-fedavg]: **Federated Averaging (FedAvg)**: Introduced by Google in 2017, FedAvg revolutionized distributed ML by averaging model weights rather than gradients. Each client performs multiple local SGD steps (typically 1-20) before sending weights to the server, reducing communication by 10-100 $\times$ compared to distributed SGD. The key insight: local updates contain richer information than single gradients, enabling convergence with far fewer communication rounds. FedAvg powers production systems like Gboard, processing billions of devices. After a fixed number of local steps, each device sends its updated model parameters to the server. The server computes a weighted average of these parameters, which are weighted according to the number of data samples on each device, and updates the global model accordingly. This updated model is then sent back to the devices, completing one round of training. Formally, let $\mathcal{D}_k$ denote the local dataset on client $k$, and let $\theta_k^t$ be the parameters of the model on client $k$ at round $t$. Each client performs $E$ steps of SGD on its local data, yielding an update $\theta_k^{t+1}$. The central server then aggregates these updates as: $$ @@ -1835,7 +1835,7 @@ where $n_k = |\mathcal{D}_k|$ is the number of samples on device $k$, $n = \sum_ #### Straggler Mitigation {#sec-edge-intelligence-straggler-mitigation-b49d} -In a fleet of millions of heterogeneous devices, waiting for every selected client to report its update is impractical. Network latency, battery depletion, or background process contention can cause some devices ("stragglers") to take 10$\times$ longer than average. +In a fleet of millions of heterogeneous devices, waiting for every selected client to report its update is impractical. Network latency, battery depletion, or background process contention can cause some devices ("stragglers") to take 10 $\times$ longer than average. To prevent the global model update from stalling, production FL systems employ **Over-Selection**. The server selects a candidate pool size $K_{candidates}$ larger than the target number of updates $K_{target}$ (typically $K_{candidates} \approx 1.3 \times K_{target}$). The server aggregates updates from the first $K_{target}$ responders and discards the rest. This approach bounds the round duration by the speed of the $K_{target}$-th fastest device rather than the absolute slowest, dramatically accelerating convergence wall-clock time. @@ -2106,9 +2106,9 @@ These techniques fall into three primary categories: model compression, selectiv Model compression methods aim to reduce the size of transmitted updates through quantization[^fn-gradient-quantization], sparsification, or subsampling. Instead of sending full-precision gradients, a client transmits 8-bit quantized updates or communicates only the top-$k$ gradient elements[^fn-gradient-sparsification] with highest magnitude. -[^fn-gradient-quantization]: **Gradient Quantization**: Reduces communication by converting FP32 gradients to lower precision (INT8, INT4, or even 1-bit). Advanced techniques like signSGD use only gradient signs, achieving 32$\times$ compression. Error compensation methods accumulate quantization errors for later transmission, maintaining convergence quality. Real deployments achieve 8-16$\times$ communication reduction with <1% accuracy loss. +[^fn-gradient-quantization]: **Gradient Quantization**: Reduces communication by converting FP32 gradients to lower precision (INT8, INT4, or even 1-bit). Advanced techniques like signSGD use only gradient signs, achieving 32 $\times$ compression. Error compensation methods accumulate quantization errors for later transmission, maintaining convergence quality. Real deployments achieve 8-16 $\times$ communication reduction with <1% accuracy loss. -[^fn-gradient-sparsification]: **Gradient Sparsification**: Transmits only the largest gradients by magnitude (typically top 1-10%), dramatically reducing communication. Gradient accumulation stores untransmitted gradients locally until they become large enough to send. This technique exploits the observation that most gradients are small and contribute minimally to convergence, achieving 10-100$\times$ compression ratios while maintaining training effectiveness. These techniques reduce transmission size with limited impact on convergence when applied carefully. +[^fn-gradient-sparsification]: **Gradient Sparsification**: Transmits only the largest gradients by magnitude (typically top 1-10%), dramatically reducing communication. Gradient accumulation stores untransmitted gradients locally until they become large enough to send. This technique exploits the observation that most gradients are small and contribute minimally to convergence, achieving 10-100 $\times$ compression ratios while maintaining training effectiveness. These techniques reduce transmission size with limited impact on convergence when applied carefully. Selective update sharing further reduces communication by transmitting only subsets of model parameters or updates. In layer-wise selective sharing, clients update only certain layers, typically the final classifier or adapter modules, while keeping the majority of the backbone frozen. This reduces both upload cost and the risk of overfitting shared representations to non-representative client data. @@ -2191,7 +2191,7 @@ Clustered federated learning offers an alternative by grouping clients according Finally, meta-learning approaches, such as Model-Agnostic Meta-Learning (MAML)[^fn-maml], aim to produce a global model initialization that can be quickly adapted to new tasks with just a few local updates [@finn2017model]. This technique is especially useful when clients have limited data or operate in environments with frequent distributional shifts. -[^fn-maml]: **Model-Agnostic Meta-Learning (MAML)**: A meta-learning algorithm (2017) that trains models not to perform well on specific tasks, but to be easily adaptable to new tasks. MAML optimizes for a model initialization from which a few gradient steps yield good performance on any task. For on-device learning, this means pre-training produces models that personalize quickly (1-5 gradient steps) from minimal user data (5-20 examples). The computational cost is high during meta-training (2-3$\times$ standard training) but minimal during deployment adaptation, making it well-suited for edge scenarios where users provide limited labeled examples. +[^fn-maml]: **Model-Agnostic Meta-Learning (MAML)**: A meta-learning algorithm (2017) that trains models not to perform well on specific tasks, but to be easily adaptable to new tasks. MAML optimizes for a model initialization from which a few gradient steps yield good performance on any task. For on-device learning, this means pre-training produces models that personalize quickly (1-5 gradient steps) from minimal user data (5-20 examples). The computational cost is high during meta-training (2-3 $\times$ standard training) but minimal during deployment adaptation, making it well-suited for edge scenarios where users provide limited labeled examples. Each of these strategies reflects a different point in the tradeoff space. Examine @tbl-personalization-strategies to see how compute overhead, privacy guarantees, and adaptation latency vary across local finetuning, personalization layers, clustered federated learning, and meta-learning approaches. @@ -2315,13 +2315,13 @@ Federated learning transforms machine learning into a massive distributed system The communication bottleneck represents the primary scalability constraint in federated learning systems. Understanding the quantitative transfer requirements enables principled design decisions about model architectures, update compression strategies, and client participation policies that determine system viability. -The federated communication hierarchy reveals the severe bandwidth constraints under which distributed learning must operate. Full model synchronization requires 10-500 MB per training round for typical deep learning models, prohibitive for mobile networks with limited upload bandwidth that averages just 5-50 Mbps in practice. Gradient compression achieves 10-100$\times$ reduction through quantization (reducing FP32 to INT8), sparsification (transmitting only non-zero gradients), and selective gradient transmission (sending only the most significant updates). Practical deployments demand even more aggressive 100-1000$\times$ compression ratios, reducing 100 MB models to manageable 100 KB-1 MB updates that mobile devices can transmit within reasonable timeframes and without exhausting data plans. Communication frequency introduces a critical trade-off between model update freshness, more frequent updates enable faster adaptation to changing conditions, and network efficiency constraints that limit sustainable bandwidth consumption. +The federated communication hierarchy reveals the severe bandwidth constraints under which distributed learning must operate. Full model synchronization requires 10-500 MB per training round for typical deep learning models, prohibitive for mobile networks with limited upload bandwidth that averages just 5-50 Mbps in practice. Gradient compression achieves 10-100 $\times$ reduction through quantization (reducing FP32 to INT8), sparsification (transmitting only non-zero gradients), and selective gradient transmission (sending only the most significant updates). Practical deployments demand even more aggressive 100-1000 $\times$ compression ratios, reducing 100 MB models to manageable 100 KB-1 MB updates that mobile devices can transmit within reasonable timeframes and without exhausting data plans. Communication frequency introduces a critical trade-off between model update freshness, more frequent updates enable faster adaptation to changing conditions, and network efficiency constraints that limit sustainable bandwidth consumption. -Network infrastructure constraints directly impact participation rates and overall system viability. Modern 4G networks typically provide upload speeds ranging from 5-50 Mbps under optimal conditions (with significant geographic and carrier variation), meaning an 8 MB model update requires 1.3-13 seconds of sustained transmission. However, real-world mobile networks exhibit extreme variability: rural areas may experience 1 Mbps upload speeds while urban 5G deployments enable 100+ Mbps. This 100$\times$ variance in network capability necessitates adaptive communication strategies that optimize for lowest-common-denominator connectivity while enabling high-capability devices to contribute more effectively. +Network infrastructure constraints directly impact participation rates and overall system viability. Modern 4G networks typically provide upload speeds ranging from 5-50 Mbps under optimal conditions (with significant geographic and carrier variation), meaning an 8 MB model update requires 1.3-13 seconds of sustained transmission. However, real-world mobile networks exhibit extreme variability: rural areas may experience 1 Mbps upload speeds while urban 5G deployments enable 100+ Mbps. This 100 $\times$ variance in network capability necessitates adaptive communication strategies that optimize for lowest-common-denominator connectivity while enabling high-capability devices to contribute more effectively. The relationship between communication requirements and participation rates exhibits sharp threshold effects. Empirical studies demonstrate that federated learning systems requiring model transfers exceeding 10 MB achieve less than 10% sustained client participation, while systems maintaining updates below 1 MB can sustain 40-60% participation rates across diverse mobile populations. This communication efficiency directly translates to model quality improvements: higher participation rates provide better statistical diversity and more robust gradient estimates for global model updates. -Advanced compression techniques become essential for practical deployment. Gradient quantization reduces precision from FP32 to INT8 or even binary representations, achieving 4-32$\times$ compression with minimal accuracy loss. Sparsification techniques transmit only the largest gradient components, leveraging the natural sparsity in neural network updates. Top-k gradient selection further reduces communication by transmitting only the most significant parameter updates, while error accumulation ensures that small gradients are not permanently lost. +Advanced compression techniques become essential for practical deployment. Gradient quantization reduces precision from FP32 to INT8 or even binary representations, achieving 4-32 $\times$ compression with minimal accuracy loss. Sparsification techniques transmit only the largest gradient components, leveraging the natural sparsity in neural network updates. Top-k gradient selection further reduces communication by transmitting only the most significant parameter updates, while error accumulation ensures that small gradients are not permanently lost. #### Asynchronous Device Synchronization {#sec-edge-intelligence-asynchronous-device-synchronization-348e} @@ -2335,13 +2335,13 @@ Fault recovery and resilience strategies form an essential layer of federated le The asynchronous nature of federated coordination introduces additional complexity in maintaining training convergence guarantees. Traditional synchronous training assumes all participants complete each round, but federated systems must handle stragglers[^fn-straggler] and dropouts gracefully. -[^fn-straggler]: **Straggler Problem in Federated Learning**: Slow or delayed clients that hold up synchronous training rounds. In datacenter distributed training, stragglers cause 10–30% throughput reduction. In federated learning, the problem is far worse: the slowest 10% of mobile devices can be 100$\times$ slower than the fastest, making synchronous protocols impractical. Solutions include asynchronous aggregation (accept updates as they arrive), bounded staleness (ignore updates older than k rounds), and proactive client selection that preferentially samples faster devices. However, aggressive straggler mitigation risks biasing models toward users with high-end devices. Techniques such as FedAsync [@xie2019fedasync] enable asynchronous aggregation where the server continuously updates the global model as client updates arrive, while bounded staleness mechanisms prevent extremely outdated updates from corrupting recent progress. +[^fn-straggler]: **Straggler Problem in Federated Learning**: Slow or delayed clients that hold up synchronous training rounds. In datacenter distributed training, stragglers cause 10–30% throughput reduction. In federated learning, the problem is far worse: the slowest 10% of mobile devices can be 100 $\times$ slower than the fastest, making synchronous protocols impractical. Solutions include asynchronous aggregation (accept updates as they arrive), bounded staleness (ignore updates older than k rounds), and proactive client selection that preferentially samples faster devices. However, aggressive straggler mitigation risks biasing models toward users with high-end devices. Techniques such as FedAsync [@xie2019fedasync] enable asynchronous aggregation where the server continuously updates the global model as client updates arrive, while bounded staleness mechanisms prevent extremely outdated updates from corrupting recent progress. #### Managing Million-Device Heterogeneity {#sec-edge-intelligence-managing-milliondevice-heterogeneity-86c1} Real-world federated learning deployments exhibit extreme heterogeneity across multiple dimensions simultaneously: hardware capabilities, network conditions, data distributions, and availability patterns. This multi-dimensional heterogeneity fundamentally challenges traditional distributed machine learning assumptions about homogeneous participants operating under similar conditions. -Real-world federated learning deployments face multi-dimensional device heterogeneity that creates extreme variation across every system dimension. Computational variation spans 1000$\times$ differences in processing power between flagship smartphones running at `{python} mobile_npu_tops` TOPS and IoT microcontrollers operating at just 0.03 TOPS, fundamentally limiting what models can train on different device tiers. Memory constraints exhibit even more dramatic 100–10,000$\times$ differences in available RAM across device categories, ranging from 256 KB on microcontrollers to 16 GB on premium smartphones, determining whether devices can perform any local training at all or must rely purely on inference. Energy limitations force training sessions to be carefully scheduled around charging patterns, thermal constraints, and battery preservation requirements, with mobile devices typically limiting ML workloads to 500–1000 mW sustained power consumption. Network diversity introduces orders-of-magnitude performance differences as WiFi, 4G, 5G, and satellite connectivity exhibit vastly different bandwidth (ranging from 1 Mbps to 1 Gbps), latency (10 ms to 600 ms), and reliability characteristics that determine feasible update frequencies and compression requirements. +Real-world federated learning deployments face multi-dimensional device heterogeneity that creates extreme variation across every system dimension. Computational variation spans 1000 $\times$ differences in processing power between flagship smartphones running at `{python} mobile_npu_tops` TOPS and IoT microcontrollers operating at just 0.03 TOPS, fundamentally limiting what models can train on different device tiers. Memory constraints exhibit even more dramatic 100–10,000 $\times$ differences in available RAM across device categories, ranging from 256 KB on microcontrollers to 16 GB on premium smartphones, determining whether devices can perform any local training at all or must rely purely on inference. Energy limitations force training sessions to be carefully scheduled around charging patterns, thermal constraints, and battery preservation requirements, with mobile devices typically limiting ML workloads to 500–1000 mW sustained power consumption. Network diversity introduces orders-of-magnitude performance differences as WiFi, 4G, 5G, and satellite connectivity exhibit vastly different bandwidth (ranging from 1 Mbps to 1 Gbps), latency (10 ms to 600 ms), and reliability characteristics that determine feasible update frequencies and compression requirements. Adaptive coordination protocols address this heterogeneity through sophisticated tiered participation strategies that optimize resource utilization across the device spectrum. High-capability devices such as flagship smartphones can perform complex local training with large batch sizes and multiple epochs, while resource-constrained IoT devices contribute through lightweight updates, specialized subtasks, or even simple data aggregation. This creates a natural computational hierarchy where powerful devices act as "super-peers" performing disproportionate computation, while edge devices contribute specialized local knowledge and coverage. @@ -2452,7 +2452,7 @@ Understanding biological solutions illuminates why certain engineering approache The human brain operates at approximately 20 watts while continuously learning from limited supervision, precisely the efficiency target for on-device learning systems[^fn-brain-efficiency]. This remarkable efficiency emerges from several architectural principles that directly inform edge learning design, demonstrating what is theoretically achievable with highly optimized learning systems. -[^fn-brain-efficiency]: **Brain Power Efficiency**: The human brain's 20 W power consumption (equivalent to a bright LED bulb) enables processing 10^15 operations per second, roughly 50,000$\times$ more efficient than current AI accelerators per operation. This efficiency comes from ~86 billion neurons with only 1-2% active simultaneously, massive parallelism with 10^14 synapses, and adaptive precision where important computations use more resources. Modern edge AI targets similar efficiency: sparse activation patterns, adaptive precision (INT8 to FP16), and event-driven processing that activates only when needed. +[^fn-brain-efficiency]: **Brain Power Efficiency**: The human brain's 20 W power consumption (equivalent to a bright LED bulb) enables processing 10^15 operations per second, roughly 50,000 $\times$ more efficient than current AI accelerators per operation. This efficiency comes from ~86 billion neurons with only 1-2% active simultaneously, massive parallelism with 10^14 synapses, and adaptive precision where important computations use more resources. Modern edge AI targets similar efficiency: sparse activation patterns, adaptive precision (INT8 to FP16), and event-driven processing that activates only when needed. The brain's efficiency characteristics reveal multiple dimensions of optimization that on-device systems should target. From a power perspective, the brain consumes just 20 W total, with approximately 10 W dedicated to active learning and memory consolidation, an energy budget comparable to what mobile devices can sustainably allocate to on-device learning during charging periods. Memory efficiency comes from sparse, distributed representations where only 1-2% of neurons activate simultaneously during any cognitive task, dramatically reducing the computational and storage requirements compared to dense neural networks. Learning efficiency manifests through few-shot learning capabilities that enable adaptation from single exposures, along with continuous adaptation mechanisms that avoid catastrophic forgetting when integrating new knowledge. Hierarchical processing organizes information across multiple scales, from low-level sensory inputs to high-level abstract reasoning, enabling efficient reuse of learned features across different tasks and contexts. @@ -2622,7 +2622,7 @@ Software heterogeneity compounds the challenge. Devices may run different versio [^fn-tflite]: **TensorFlow Lite**: Google's framework for mobile and embedded ML inference, optimized for ARM processors and mobile GPUs. TFLite reduces model size by 75% through quantization and pruning, while achieving $3\times$ faster inference than full TensorFlow. The framework supports 16-bit and 8-bit quantization, with specialized kernels for mobile CPUs and GPUs. TFLite Micro targets microcontrollers with <1 MB memory, enabling ML on Arduino and other embedded platforms. -[^fn-onnx-runtime-mobile]: **ONNX Runtime Mobile**: Microsoft's cross-platform inference engine for the Open Neural Network Exchange format. ONNX provides vendor-neutral model representation, enabling training in PyTorch and deployment on iOS, Android, or embedded Linux without framework-specific code. The mobile variant achieves 2-3$\times$ faster inference than TensorFlow Lite on certain models through aggressive operator fusion and platform-specific optimizations. Critical for federated learning deployments where a single model must run identically across heterogeneous devices with different ML frameworks. +[^fn-onnx-runtime-mobile]: **ONNX Runtime Mobile**: Microsoft's cross-platform inference engine for the Open Neural Network Exchange format. ONNX provides vendor-neutral model representation, enabling training in PyTorch and deployment on iOS, Android, or embedded Linux without framework-specific code. The mobile variant achieves 2-3 $\times$ faster inference than TensorFlow Lite on certain models through aggressive operator fusion and platform-specific optimizations. Critical for federated learning deployments where a single model must run identically across heterogeneous devices with different ML frameworks. In addition to computational heterogeneity, devices exhibit variation in connectivity and uptime. Some are intermittently connected, plugged in only occasionally, or operate under strict bandwidth constraints. Others may have continuous power and reliable networking, but still prioritize user-facing responsiveness over background learning. These differences complicate the orchestration of coordinated learning and the scheduling of updates. @@ -2664,7 +2664,7 @@ Beyond traditional inference metrics, adaptive systems require specialized train Evaluating whether local adaptation actually improves over global models requires personalization gain metrics that justify the overhead of on-device learning. Per-user performance delta measures accuracy improvement for the adapted model versus the global baseline on user-specific holdout data, systems should demonstrate statistically significant improvements, typically exceeding 2% accuracy gains, to justify the computational overhead, energy consumption, and complexity that adaptation introduces. Personalization-privacy tradeoff quantifies accuracy gain per unit of local data exposure, measuring the value extracted from privacy-sensitive information, this metric helps assess whether adaptation benefits outweigh the privacy costs of retaining user data locally, particularly important for applications handling sensitive information like health data or personal communications. Catastrophic forgetting rate measures degradation on the original task as the model adapts to local distributions through retention testing, acceptable forgetting rates depend on the application domain but typically should remain below 5% accuracy loss on original tasks to ensure that personalization does not come at the expense of the model's general capabilities. -When devices coordinate through the federated protocols examined in @sec-edge-intelligence-federated-learning-6e7e, federated coordination cost metrics become critical for assessing system viability. Communication efficiency measures model accuracy improvement per byte transmitted, capturing the effectiveness of gradient compression and selective update strategies, modern federated systems achieve 10-100$\times$ compression through quantization and sparsification techniques while maintaining 95% or more of uncompressed accuracy, making the difference between practical and impractical mobile deployment. Stragglers impact quantifies convergence delay caused by slow or unreliable devices, measured as the difference in convergence time with versus without participation filters, effective straggler mitigation through asynchronous aggregation and selective participation reduces convergence time by 30-50% compared to synchronous approaches that wait for all devices. Aggregation quality evaluates global model performance as a function of device participation rate, revealing minimum viable participation thresholds below which federated learning fails to converge effectively, most federated systems require 10-20% device participation per round to maintain stable convergence, establishing clear requirements for client selection and availability management strategies. +When devices coordinate through the federated protocols examined in @sec-edge-intelligence-federated-learning-6e7e, federated coordination cost metrics become critical for assessing system viability. Communication efficiency measures model accuracy improvement per byte transmitted, capturing the effectiveness of gradient compression and selective update strategies, modern federated systems achieve 10-100 $\times$ compression through quantization and sparsification techniques while maintaining 95% or more of uncompressed accuracy, making the difference between practical and impractical mobile deployment. Stragglers impact quantifies convergence delay caused by slow or unreliable devices, measured as the difference in convergence time with versus without participation filters, effective straggler mitigation through asynchronous aggregation and selective participation reduces convergence time by 30-50% compared to synchronous approaches that wait for all devices. Aggregation quality evaluates global model performance as a function of device participation rate, revealing minimum viable participation thresholds below which federated learning fails to converge effectively, most federated systems require 10-20% device participation per round to maintain stable convergence, establishing clear requirements for client selection and availability management strategies. These training-specific benchmarks complement inference metrics including latency, throughput, and accuracy, creating complete performance characterization for adaptive systems. Practical benchmarking must measure both dimensions: a system that achieves fast inference but slow adaptation, or efficient adaptation but poor final accuracy, fails to meet real-world requirements. The integration of inference and training benchmarks enables holistic evaluation of on-device learning systems across their full operational lifecycle. @@ -2680,7 +2680,7 @@ Energy consumption compounds this problem. Adaptation typically involves sustain From a memory perspective, training incurs higher peak usage than inference, due to the need to cache intermediate activations[^fn-activation-caching], gradients, and optimizer state [@lin2020mcunet]. -[^fn-activation-caching]: **Activation Caching**: During backpropagation, forward pass activations must be stored to compute gradients, dramatically increasing memory usage. For a typical CNN, activation memory can be 3-5$\times$ larger than model weights. Modern techniques like gradient checkpointing trade computation for memory by recomputing activations during backward pass, reducing memory by 80% at the cost of ~30% more compute time. Critical for training on memory-constrained devices where activation storage often exceeds available RAM. These requirements may exceed the static memory footprint anticipated during model deployment, particularly when adaptation involves multiple layers or gradient accumulation. In highly constrained systems, for example, systems with less than 512 KB of RAM, this may preclude certain types of adaptation altogether, unless additional optimization techniques (e.g., checkpointing or low-rank updates) are employed. +[^fn-activation-caching]: **Activation Caching**: During backpropagation, forward pass activations must be stored to compute gradients, dramatically increasing memory usage. For a typical CNN, activation memory can be 3-5 $\times$ larger than model weights. Modern techniques like gradient checkpointing trade computation for memory by recomputing activations during backward pass, reducing memory by 80% at the cost of ~30% more compute time. Critical for training on memory-constrained devices where activation storage often exceeds available RAM. These requirements may exceed the static memory footprint anticipated during model deployment, particularly when adaptation involves multiple layers or gradient accumulation. In highly constrained systems, for example, systems with less than 512 KB of RAM, this may preclude certain types of adaptation altogether, unless additional optimization techniques (e.g., checkpointing or low-rank updates) are employed. These resource demands must also be balanced against quality of service (QoS)[^fn-qos-edge] goals. Users expect edge devices to respond reliably and consistently, regardless of whether learning is occurring in the background. Any observable degradation, including dropped audio in a wake-word detector or lag in a wearable display, can erode user trust. @@ -2752,7 +2752,7 @@ First, unlike centralized systems where failures are localized and observable, o Second, the federated coordination mechanisms that enable collaborative learning also create new attack surfaces. Adversarial clients can inject poisoned gradients[^fn-byzantine-fault-tolerance] designed to degrade global model performance. Model inversion attacks can extract private information from shared updates despite aggregation. The distributed nature of on-device learning makes these attacks both easier to execute (compromising client devices) and harder to detect (no centralized validation). -[^fn-byzantine-fault-tolerance]: **Byzantine Fault Tolerance in FL**: Distributed systems property that enables correct operation despite some participants being malicious or faulty (named after the Byzantine Generals Problem). In federated learning, up to f malicious clients can be tolerated among n participants using algorithms like Krum or trimmed mean aggregation, which requires n ≥ 3f + 1 total participants. These robust aggregation methods increase communication costs by 2-5$\times$ and computational overhead by 3-10$\times$, but prevent poisoning attacks where malicious clients could degrade global model performance by injecting adversarial gradients. +[^fn-byzantine-fault-tolerance]: **Byzantine Fault Tolerance in FL**: Distributed systems property that enables correct operation despite some participants being malicious or faulty (named after the Byzantine Generals Problem). In federated learning, up to f malicious clients can be tolerated among n participants using algorithms like Krum or trimmed mean aggregation, which requires n ≥ 3f + 1 total participants. These robust aggregation methods increase communication costs by 2-5 $\times$ and computational overhead by 3-10 $\times$, but prevent poisoning attacks where malicious clients could degrade global model performance by injecting adversarial gradients. Third, on-device systems must handle distribution shifts and environmental changes without access to labeled validation data. Models may confidently drift into failure modes, adapting to local biases or temporary anomalies. The non-IID data distributions across devices mean that local drift on individual devices may not trigger global alarms, allowing silent degradation. @@ -2766,7 +2766,7 @@ On-device learning operates in a fundamentally different environment from cloud- **Fallacy:** ***On-device learning provides the same adaptation capabilities as cloud-based training.*** -Teams expect local learning to match centralized training's model improvements, ignoring fundamental resource constraints. On-device learning amplifies resource needs by 3-10$\times$ compared to inference-only deployment due to activation caching, gradient storage, and bidirectional memory traffic (@sec-edge-intelligence-model-adaptation-6a82). Local datasets are typically small, biased, and non-representative, while compute budgets remain orders of magnitude below cloud GPUs. A smartphone with 8GB RAM and 5W power budget cannot replicate the adaptation achieved by datacenter systems with terabytes of memory and kilowatts of power. Effective on-device learning requires designing adaptation strategies that provide meaningful improvements within these constraints rather than attempting to replicate cloud-scale learning capabilities. +Teams expect local learning to match centralized training's model improvements, ignoring fundamental resource constraints. On-device learning amplifies resource needs by 3-10 $\times$ compared to inference-only deployment due to activation caching, gradient storage, and bidirectional memory traffic (@sec-edge-intelligence-model-adaptation-6a82). Local datasets are typically small, biased, and non-representative, while compute budgets remain orders of magnitude below cloud GPUs. A smartphone with 8GB RAM and 5W power budget cannot replicate the adaptation achieved by datacenter systems with terabytes of memory and kilowatts of power. Effective on-device learning requires designing adaptation strategies that provide meaningful improvements within these constraints rather than attempting to replicate cloud-scale learning capabilities. **Pitfall:** ***Assuming that federated learning automatically preserves privacy without additional safeguards.*** @@ -2778,7 +2778,7 @@ This belief assumes that any local adaptation is beneficial regardless of the qu **Pitfall:** ***Ignoring the heterogeneity challenges across different device types and capabilities.*** -Teams design on-device learning systems assuming uniform hardware capabilities across deployment devices. Real-world deployments span diverse hardware with varying computational power, memory capacity, energy constraints, and networking capabilities. Edge device capabilities span 6+ orders of magnitude: from 32 KB RAM microcontrollers to 16 GB smartphones, 48MHz ARM Cortex-M0+ (~10 MIPS) to 3GHz A-series processors (~100,000 MIPS), and 10μW sensor nodes to 5W flagship phones. A learning algorithm that works well on high-end smartphones may fail catastrophically on resource-constrained IoT devices. Federated learning algorithms must dynamically adapt through quantized inference on low-end devices, selective participation based on capability, and tiered aggregation strategies that account for 10,000$\times$ performance differences within a single deployment. +Teams design on-device learning systems assuming uniform hardware capabilities across deployment devices. Real-world deployments span diverse hardware with varying computational power, memory capacity, energy constraints, and networking capabilities. Edge device capabilities span 6+ orders of magnitude: from 32 KB RAM microcontrollers to 16 GB smartphones, 48MHz ARM Cortex-M0+ (~10 MIPS) to 3GHz A-series processors (~100,000 MIPS), and 10μW sensor nodes to 5W flagship phones. A learning algorithm that works well on high-end smartphones may fail catastrophically on resource-constrained IoT devices. Federated learning algorithms must dynamically adapt through quantized inference on low-end devices, selective participation based on capability, and tiered aggregation strategies that account for 10,000 $\times$ performance differences within a single deployment. **Pitfall:** ***Underestimating the complexity of orchestrating learning across distributed edge systems.*** @@ -2792,8 +2792,8 @@ We established a three-pillar framework for navigating these constraints: **Mode ::: {.callout-takeaways} -* **The Mobile Memory Wall**: While NPUs provide raw compute (TOPS), on-device LLMs are strictly bandwidth-bound. The 30–50$\times$ gap between mobile RAM and datacenter HBM makes quantization a mandatory "bandwidth survival strategy" for interactive decode speeds. -* **Training is Not Inference**: On-device learning amplifies resource needs by 3–10$\times$ compared to inference-only deployment, due to activation caching, gradient storage, and bidirectional memory traffic. +* **The Mobile Memory Wall**: While NPUs provide raw compute (TOPS), on-device LLMs are strictly bandwidth-bound. The 30–50 $\times$ gap between mobile RAM and datacenter HBM makes quantization a mandatory "bandwidth survival strategy" for interactive decode speeds. +* **Training is Not Inference**: On-device learning amplifies resource needs by 3–10 $\times$ compared to inference-only deployment, due to activation caching, gradient storage, and bidirectional memory traffic. * **The Three Pillars**: Resource-constrained learning depends on Model Adaptation (bias-only/sparse updates), Data Efficiency (few-shot/experience replay), and Federated Coordination (privacy-preserving aggregation). * **Heterogeneity is the Default**: Unlike uniform datacenter racks, the edge is a fragmented ecosystem of microcontrollers and SoCs. Federated learning must account for "stragglers" and participation bias generated by these physical disparities. ::: diff --git a/book/quarto/contents/vol2/emerging_challenges/emerging_challenges.qmd b/book/quarto/contents/vol2/emerging_challenges/emerging_challenges.qmd index 5ebe07b3ee..1967c4b493 100644 --- a/book/quarto/contents/vol2/emerging_challenges/emerging_challenges.qmd +++ b/book/quarto/contents/vol2/emerging_challenges/emerging_challenges.qmd @@ -185,7 +185,7 @@ This suggests a new scaling dimension: rather than solely investing compute in l The uncertainty increases substantially for extrapolation beyond tested regimes. Some researchers extrapolate that continued scaling would require approximately 2.5 $\times$ 10²⁶ FLOPs[^fn-compute-extrapolation] to achieve qualitatively different capabilities. However, this extrapolation assumes: (1) scaling laws continue beyond tested regimes, (2) capability improvements track loss improvements, and (3) no fundamental architectural constraints exist. **Each assumption carries substantial uncertainty**, and historical AI predictions show systematic overconfidence in extrapolation. -[^fn-compute-extrapolation]: **Compute Extrapolation Uncertainty**: Some estimates project 2.5 $\times$ 10²⁶ FLOPs would yield qualitatively different capabilities, but this involves extrapolating 100-1000$\times$ beyond tested regimes. At current H100 efficiency, this would require: 175,000 GPUs for one year, 122 MW power consumption (comparable to 100,000 US homes), and approximately $52 billion total cost. **Uncertainty bounds**: Actual requirements could differ by 1-3 orders of magnitude in either direction. Historical AI capability predictions show median errors of 10-100$\times$ on 5-10 year timescales [@grace2018will]. +[^fn-compute-extrapolation]: **Compute Extrapolation Uncertainty**: Some estimates project 2.5 $\times$ 10²⁶ FLOPs would yield qualitatively different capabilities, but this involves extrapolating 100-1000 $\times$ beyond tested regimes. At current H100 efficiency, this would require: 175,000 GPUs for one year, 122 MW power consumption (comparable to 100,000 US homes), and approximately $52 billion total cost. **Uncertainty bounds**: Actual requirements could differ by 1-3 orders of magnitude in either direction. Historical AI capability predictions show median errors of 10-100 $\times$ on 5-10 year timescales [@grace2018will]. #### Quantitative Limitations of Scaling {#sec-emerging-systems-challenges-quantitative-limitations-scaling-4c0a} @@ -195,11 +195,11 @@ Critical evaluation reveals four fundamental limitations that constrain scaling For instance, multi-digit addition appears suddenly when models reach sufficient scale, and in-context learning exhibits threshold behavior rather than gradual improvement. These discontinuities raise a critical question: are these genuine phase transitions where new capabilities emerge, or are they evaluation artifacts where smooth capability improvements cross arbitrary metric thresholds? Schaeffer et al. (2023) demonstrate that many reported "emergent" capabilities result from discontinuous evaluation metrics (like exact match accuracy) rather than fundamental capability phase transitions. When evaluated with continuous metrics (like token edit distance), the same capabilities show smooth, predictable improvements consistent with scaling laws. This distinction matters profoundly: if capabilities emerge smoothly but metrics measure discontinuously, then scaling provides predictable improvements. If capabilities genuinely exhibit phase transitions, extrapolation becomes speculative. -**Data Scaling Hits Quantifiable Walls.** High-quality text data faces exhaustion before capability plateaus, creating a measurable constraint on scaling strategies. Current estimates place internet-scale high-quality text at approximately 10¹³ tokens (web articles, books, academic papers, code repositories), with uncertainty bounds of 0.5-2$\times$ [@villalobos2022will]. +**Data Scaling Hits Quantifiable Walls.** High-quality text data faces exhaustion before capability plateaus, creating a measurable constraint on scaling strategies. Current estimates place internet-scale high-quality text at approximately 10¹³ tokens (web articles, books, academic papers, code repositories), with uncertainty bounds of 0.5-2 $\times$ [@villalobos2022will]. -**Chinchilla optimal scaling** [@hoffmann2022training] establishes that compute-efficient training requires balanced scaling of parameters and data: $D \approx 20N$, meaning tokens should equal approximately 20$\times$ parameters. This finding fundamentally changed the economics of model training. GPT-3 (`{python} gpt3_params_label` parameters, 300B tokens) had a Chinchilla ratio of only 1.7, making it substantially under-trained. Chinchilla (70B parameters, 1.4T tokens) achieved a ratio of 20 and outperformed the much larger GPT-3. Current frontier models like LLaMA-2-70B (70B parameters, 2T tokens) operate near-optimally with ratios around 29. +**Chinchilla optimal scaling** [@hoffmann2022training] establishes that compute-efficient training requires balanced scaling of parameters and data: $D \approx 20N$, meaning tokens should equal approximately 20 $\times$ parameters. This finding fundamentally changed the economics of model training. GPT-3 (`{python} gpt3_params_label` parameters, 300B tokens) had a Chinchilla ratio of only 1.7, making it substantially under-trained. Chinchilla (70B parameters, 1.4T tokens) achieved a ratio of 20 and outperformed the much larger GPT-3. Current frontier models like LLaMA-2-70B (70B parameters, 2T tokens) operate near-optimally with ratios around 29. -The constraint becomes concrete: training a 100 trillion parameter model at Chinchilla-optimal ratio would require $2 \times 10^{15}$ tokens, representing a 200$\times$ shortfall compared to estimated available high-quality data. **This is not a speculative future problem but a current constraint affecting infrastructure decisions.** +The constraint becomes concrete: training a 100 trillion parameter model at Chinchilla-optimal ratio would require $2 \times 10^{15}$ tokens, representing a 200 $\times$ shortfall compared to estimated available high-quality data. **This is not a speculative future problem but a current constraint affecting infrastructure decisions.** This creates a quantitative dilemma with four options, each with measurable trade-offs. First, train data-starved models that cannot utilize parameter capacity, accepting 10-30% higher loss than Chinchilla-optimal. Second, generate synthetic training data using models, risking quality degradation and potential model collapse (documented degradation of 5-15% on reasoning benchmarks after multiple generations). Third, include lower-quality data (noisy web content, social media), accepting 3-8% performance degradation on quality-sensitive tasks. Fourth, accept parameter limits around current model sizes, investing instead in architectural innovation. @@ -207,9 +207,9 @@ Each option has measurable consequences for training efficiency, model capabilit **Inference-Time vs. Training-Time Scaling.** OpenAI's o1 and o3 models demonstrate that allocating compute during inference provides different trade-offs than training larger models. Training-time scaling improves pre-training loss through increased model capacity. Inference-time scaling performs task-specific computation: searching over possible solutions, verifying candidate answers, and refining outputs through iterative improvement. -This distinction carries measurable economic implications. Training-time scaling follows power laws with diminishing returns: doubling parameters reduces loss by progressively smaller amounts. Inference-time scaling has different cost structures: 10-100$\times$ more compute per query, but only for queries that benefit from extended reasoning. For mathematics, generating candidate proofs and verifying them through symbolic checkers enables solving problems where direct generation fails. For coding, generating multiple programs and executing against test suites ensures correctness independent of generation accuracy. +This distinction carries measurable economic implications. Training-time scaling follows power laws with diminishing returns: doubling parameters reduces loss by progressively smaller amounts. Inference-time scaling has different cost structures: 10-100 $\times$ more compute per query, but only for queries that benefit from extended reasoning. For mathematics, generating candidate proofs and verifying them through symbolic checkers enables solving problems where direct generation fails. For coding, generating multiple programs and executing against test suites ensures correctness independent of generation accuracy. -**Quantitative trade-off**: A model using 100$\times$ inference compute on complex queries costs 100$\times$ more per such query. Whether this trade-off is economically favorable depends on: (1) the fraction of queries requiring extended reasoning, (2) the value difference between correct and incorrect answers, and (3) the cost of alternative approaches like human verification. **There is no universal answer**; the optimal balance is workload-specific. +**Quantitative trade-off**: A model using 100 $\times$ inference compute on complex queries costs 100 $\times$ more per such query. Whether this trade-off is economically favorable depends on: (1) the fraction of queries requiring extended reasoning, (2) the value difference between correct and incorrect answers, and (3) the cost of alternative approaches like human verification. **There is no universal answer**; the optimal balance is workload-specific. The systems implications are concrete. Training-time scaling requires datacenter-scale distributed training infrastructure optimized for throughput. Inference-time scaling demands different optimizations: latency-sensitive serving for interactive search, speculative execution for candidate generation, and efficient verification for correctness checking. This suggests systems will increasingly resemble compound architectures coordinating specialized components rather than monolithic models, but this is an engineering observation about current constraints, not a prediction about future capabilities. @@ -271,7 +271,7 @@ RT-2 (Robotics Transformer 2) [@rt2023robotics] demonstrates early progress brid Embodied systems face unique engineering constraints absent in purely digital intelligence. Real-time control loops demand sub-100 ms inference latency for stable manipulation, requiring on-device deployment from @sec-edge-intelligence rather than cloud inference where network round-trip latency alone exceeds control budgets. The control hierarchy operates at multiple timescales: high-level task planning (10-100 Hz, "grasp the cup"), mid-level motion planning (100-1000 Hz, trajectory generation), and low-level control (1000+ Hz, motor commands with proprioceptive feedback). Each layer must complete inference within its cycle time while maintaining safety constraints that prevent self-collision, workspace violations, or excessive forces that could damage objects or injure humans. -Power constraints impose severe limitations compared to datacenter systems. A mobile robot operates on 100-500 W total power budget (batteries, actuators, sensors, computation) versus a datacenter's megawatts for model inference alone. Boston Dynamics' Atlas humanoid robot dedicates approximately 1 kW to hydraulic actuation and 100-200W to onboard computation, forcing aggressive model compression and efficient architectures. This drives neuromorphic computing interest: Intel's Loihi [@davies2018loihi] processes visual attention tasks at 1000$\times$ lower power than GPUs, making it viable for battery-powered systems. The power-performance trade-off becomes critical: running a 7B parameter model at 10 Hz for real-time inference requires 50-100W on mobile GPUs, consuming substantial battery capacity that reduces operational time from hours to minutes. +Power constraints impose severe limitations compared to datacenter systems. A mobile robot operates on 100-500 W total power budget (batteries, actuators, sensors, computation) versus a datacenter's megawatts for model inference alone. Boston Dynamics' Atlas humanoid robot dedicates approximately 1 kW to hydraulic actuation and 100-200W to onboard computation, forcing aggressive model compression and efficient architectures. This drives neuromorphic computing interest: Intel's Loihi [@davies2018loihi] processes visual attention tasks at 1000 $\times$ lower power than GPUs, making it viable for battery-powered systems. The power-performance trade-off becomes critical: running a 7B parameter model at 10 Hz for real-time inference requires 50-100W on mobile GPUs, consuming substantial battery capacity that reduces operational time from hours to minutes. Safety-critical operation necessitates formal verification methods beyond the statistical guarantees of pure learning systems. When Tesla's Full Self-Driving operates on public roads or surgical robots manipulate near vital organs, probabilistic guarantees prove insufficient. Embodied AGI requires certified behavior: provable bounds on states the system can enter, guaranteed response times for emergency stops, and verified fallback behaviors when learning-based components fail. This motivates hybrid architectures combining learned policies for nominal operation with hard-coded safety controllers that activate on anomaly detection, verified through formal methods proving the combined system satisfies safety specifications. The verification challenge intensifies with learning: continual adaptation from experience must preserve safety properties even as policies evolve. @@ -699,7 +699,7 @@ teal!20!gray/{Code\\ Interpreter\\{\fontsize{7pt}{7}\selectfont \textcolor{Brown Data engineering represents the first and most critical building block. Compound AI systems require advanced data engineering to feed their specialized components, yet machine learning faces a data availability crisis. The scale becomes apparent when examining model requirements progression: GPT-3 consumed 300 billion tokens (OpenAI), GPT-4 likely used over 10 trillion tokens (scaling law extrapolations[^fn-chinchilla-laws]), yet research estimates suggest only 4.6-17 trillion high-quality tokens exist across the entire internet[^fn-data-availability-crisis]. This progression reveals a critical bottleneck: at current consumption rates, traditional web-scraped text data may be exhausted by 2026, forcing exploration of synthetic data generation and alternative scaling paths [@epoch2022compute]. -[^fn-chinchilla-laws]: **Chinchilla Scaling Laws**: Discovered by DeepMind [@hoffmann2022training], optimal model performance requires balanced scaling of parameters N and training tokens D following N ∝ D^0.74. Previous models were under-trained: GPT-3 (`{python} gpt3_params_label` parameters, 300B tokens) should have used 4.6 trillion tokens for optimal performance. Chinchilla (70B parameters, 1.4T tokens) outperformed GPT-3 despite being 2.5$\times$ smaller, proving data quality matters more than model size. +[^fn-chinchilla-laws]: **Chinchilla Scaling Laws**: Discovered by DeepMind [@hoffmann2022training], optimal model performance requires balanced scaling of parameters N and training tokens D following N ∝ D^0.74. Previous models were under-trained: GPT-3 (`{python} gpt3_params_label` parameters, 300B tokens) should have used 4.6 trillion tokens for optimal performance. Chinchilla (70B parameters, 1.4T tokens) outperformed GPT-3 despite being 2.5 $\times$ smaller, proving data quality matters more than model size. [^fn-data-availability-crisis]: **Data Availability Crisis**: High-quality training data may be exhausted by 2026. While GPT-3 used 300B tokens and GPT-4 likely used over 10T tokens, researchers estimate only 4.6-17T high-quality tokens exist across the entire internet. This progression reveals a critical bottleneck requiring exploration of synthetic data generation and alternative scaling approaches. @@ -715,7 +715,7 @@ The biological precedent proves informative. Human brains process approximately Yann LeCun calls self-supervised learning the "dark matter" of intelligence [@lecun2022path], invisible yet constituting most of the learning universe. Current language models barely scratch this surface through next-token prediction, a primitive form that learns statistical correlations rather than causal understanding. When ChatGPT predicts "apple" after "red," it uses co-occurrence statistics, not an understanding that apples possess the property of redness. -The Joint Embedding Predictive Architecture (JEPA)[^fn-jepa] demonstrates a more sophisticated approach. Instead of predicting raw pixels or tokens, JEPA learns abstract representations of world states. Shown a video of a ball rolling down a ramp, JEPA doesn't predict pixel values frame-by-frame. Instead, it learns representations encoding trajectory, momentum, and collision dynamics, concepts transferable across different objects and scenarios. This abstraction achieves 3$\times$ better sample efficiency than pixel prediction while learning genuinely reusable knowledge. +The Joint Embedding Predictive Architecture (JEPA)[^fn-jepa] demonstrates a more sophisticated approach. Instead of predicting raw pixels or tokens, JEPA learns abstract representations of world states. Shown a video of a ball rolling down a ramp, JEPA doesn't predict pixel values frame-by-frame. Instead, it learns representations encoding trajectory, momentum, and collision dynamics, concepts transferable across different objects and scenarios. This abstraction achieves 3 $\times$ better sample efficiency than pixel prediction while learning genuinely reusable knowledge. [^fn-jepa]: **Joint Embedding Predictive Architecture (JEPA)**: Meta AI's framework [@lecun2022path] for learning abstract world models. V-JEPA [@bardes2024vjepa] learns object permanence and physics from video alone, without labels or rewards. Key innovation: predicting in latent space rather than pixel space, similar to how humans imagine scenarios abstractly rather than visualizing every detail. @@ -739,7 +739,7 @@ $$\mathcal{L}_{contrastive} = -\log \frac{\exp(\text{sim}(z_i, z_j) / \tau)}{\su where $z_i = h(f_\theta(x_i))$ projects representations through encoder $f_\theta$ and projection head $h$, $\text{sim}(u,v) = u^\top v / \|u\| \|v\|$ computes cosine similarity, and $\tau$ is a temperature parameter controlling concentration. The numerator encourages similar representations for augmented views of the same input (positive pairs), while the denominator pushes apart representations from different inputs (negative pairs). -This formulation provides several quantitative advantages. First, it scales with unlabeled data: training on 1 billion images achieves 5-10% better downstream accuracy than 1 million images, with improvements continuing beyond supervised learning's saturation point. Second, it learns transferable representations: a model pre-trained contrastively on ImageNet achieves 92% of supervised performance when fine-tuned on target tasks using only 1% of labeled data, demonstrating 100$\times$ label efficiency improvement. Third, the learned representations encode semantic similarity: objects from the same category cluster in embedding space even without category labels during pre-training, with 72% accuracy on nearest-neighbor classification despite zero labeled training examples. +This formulation provides several quantitative advantages. First, it scales with unlabeled data: training on 1 billion images achieves 5-10% better downstream accuracy than 1 million images, with improvements continuing beyond supervised learning's saturation point. Second, it learns transferable representations: a model pre-trained contrastively on ImageNet achieves 92% of supervised performance when fine-tuned on target tasks using only 1% of labeled data, demonstrating 100 $\times$ label efficiency improvement. Third, the learned representations encode semantic similarity: objects from the same category cluster in embedding space even without category labels during pre-training, with 72% accuracy on nearest-neighbor classification despite zero labeled training examples. **Masked Prediction Framework.** Instead of contrasting examples, masked prediction reconstructs corrupted inputs. Given input $x$, mask a subset $M \subset \{1, ..., |x|\}$ and predict masked content from visible context: @@ -757,11 +757,11 @@ $$\mathcal{L}_{JEPA} = \mathbb{E}_{x,t}[\|s_\theta(x_t) - z_\theta(x_{t-\Delta t where $s_\theta$ computes target representations from future states $x_t$, $z_\theta$ encodes context from past states $x_{t-\Delta t}$, and prediction occurs in embedding space rather than pixel/token space. The critical difference: by predicting abstract representations rather than raw observations, JEPA avoids modeling irrelevant details. When predicting video frames, pixel-level prediction must account for texture patterns, lighting variations, and background clutter. Representation-level prediction ignores such details, focusing on semantically relevant dynamics like object motion and interaction. -This abstraction provides measurable efficiency gains. V-JEPA processes 2000 video frames to learn object permanence and basic physics, while pixel-prediction models require 10,000+ frames for equivalent understanding (5$\times$ sample efficiency improvement). The learned representations transfer across contexts: trained on balls rolling down ramps, JEPA generalizes to predicting trajectories for novel objects without retraining, demonstrating compositional understanding impossible for pixel-level models that memorize specific visual patterns. +This abstraction provides measurable efficiency gains. V-JEPA processes 2000 video frames to learn object permanence and basic physics, while pixel-prediction models require 10,000+ frames for equivalent understanding (5 $\times$ sample efficiency improvement). The learned representations transfer across contexts: trained on balls rolling down ramps, JEPA generalizes to predicting trajectories for novel objects without retraining, demonstrating compositional understanding impossible for pixel-level models that memorize specific visual patterns. **Sample Complexity Analysis.** Self-supervised learning's fundamental advantage lies in sample complexity. Supervised learning requires $O(d/\epsilon^2)$ labeled examples to learn a classifier with $d$ dimensions and error $\epsilon$ (VC dimension bound). Self-supervised learning amortizes this cost: pre-train once on $10^9$ unlabeled examples, then fine-tune for specific tasks using $10^2$-$10^3$ labeled examples. The effective sample complexity becomes $O(d_{pretrain}/\epsilon_{pretrain}^2 + d_{finetune}/\epsilon_{finetune}^2)$ where typically $d_{pretrain} \gg d_{finetune}$, enabling massive labeled data reduction for downstream tasks. -Empirically, this manifests as label efficiency. ImageNet classification requires 1.28M labeled images to achieve 76% supervised accuracy. With self-supervised pre-training, the same 76% accuracy requires only 12,800 labeled images (100$\times$ reduction), and 1% labeled data achieves 72% accuracy. For language understanding, GPT-3 achieves few-shot learning: providing 10-100 examples in context enables task performance approaching supervised fine-tuning on thousands of labeled examples, representing 10-100$\times$ label efficiency improvement from self-supervised pre-training on web text. +Empirically, this manifests as label efficiency. ImageNet classification requires 1.28M labeled images to achieve 76% supervised accuracy. With self-supervised pre-training, the same 76% accuracy requires only 12,800 labeled images (100 $\times$ reduction), and 1% labeled data achieves 72% accuracy. For language understanding, GPT-3 achieves few-shot learning: providing 10-100 examples in context enables task performance approaching supervised fine-tuning on thousands of labeled examples, representing 10-100 $\times$ label efficiency improvement from self-supervised pre-training on web text. For compound systems, self-supervised learning enables each specialized component to develop expertise from its natural data domain. A vision module learns from images, a language module from text, a dynamics module from video, all without manual labeling. The engineering challenge involves coordinating these diverse learning processes: ensuring representations align across modalities, preventing catastrophic forgetting when components update, and maintaining consistency as the system scales. Frameworks such as PyTorch, TensorFlow, and JAX must evolve to support these heterogeneous self-supervised objectives within unified training loops. @@ -771,7 +771,7 @@ Compound systems generate their own training data through guided synthesis rathe First, verification through external ground truth constrains generation. Microsoft's Phi models [@gunasekar2023textbooks] generate synthetic textbook problems but verify solutions through symbolic execution, mathematical proof checkers, or code compilation. A generated algebra problem must have a unique, verifiable solution; a programming exercise must compile and pass test cases. This creates a feedback loop where generators learn to produce not merely plausible examples but verifiable correct ones. -Second, curriculum-based synthesis starts with simple, tractable examples and progressively increases complexity. Phi-2 (2.7B parameters) matches GPT-3.5 (`{python} gpt3_params_label`) performance because its synthetic training data follows pedagogical progression: basic arithmetic before calculus, simple functions before recursion, concrete examples before abstract reasoning. This structured curriculum enables smaller models to achieve capabilities requiring 65$\times$ more parameters when trained on unstructured web data. +Second, curriculum-based synthesis starts with simple, tractable examples and progressively increases complexity. Phi-2 (2.7B parameters) matches GPT-3.5 (`{python} gpt3_params_label`) performance because its synthetic training data follows pedagogical progression: basic arithmetic before calculus, simple functions before recursion, concrete examples before abstract reasoning. This structured curriculum enables smaller models to achieve capabilities requiring 65 $\times$ more parameters when trained on unstructured web data. Third, ensemble verification uses multiple independent models to filter synthetic data. When generating training examples, outputs must satisfy multiple distinct critic models trained on different data distributions. This prevents systematic biases: if one generator consistently produces examples favoring particular patterns, ensemble critics trained on diverse data will identify and reject these biased samples. Anthropic's Constitutional AI demonstrates this through iterative refinement: one component generates responses, multiple critics evaluate them against different principles (helpfulness, harmlessness, factual accuracy), and synthesis produces improved versions satisfying all criteria simultaneously. @@ -1012,13 +1012,13 @@ These observations motivate architectural designs enabling selective activation The Mixture of Experts (MoE)[^fn-moe] architecture [@fedus2022switch] demonstrates the compound systems principle at the model level: specialized components activated through intelligent routing. Rather than processing every input through all parameters, MoE models consist of multiple expert networks, each specializing in different problem types. -[^fn-moe]: **Mixture of Experts (MoE)**: Sparse architecture where only a subset of model parameters (experts) activate per input. Originated in 1991 with Jacobs et al., but became practical at scale with Google's Switch Transformer (2021). Key systems benefit: MoE models can have 8$\times$ more parameters than dense models with comparable inference cost, since only 12-25% of parameters activate per token. Challenge: expert load balancing requires auxiliary loss functions, and irregular memory access patterns reduce GPU utilization from 80% to 40-60% without careful optimization. +[^fn-moe]: **Mixture of Experts (MoE)**: Sparse architecture where only a subset of model parameters (experts) activate per input. Originated in 1991 with Jacobs et al., but became practical at scale with Google's Switch Transformer (2021). Key systems benefit: MoE models can have 8 $\times$ more parameters than dense models with comparable inference cost, since only 12-25% of parameters activate per token. Challenge: expert load balancing requires auxiliary loss functions, and irregular memory access patterns reduce GPU utilization from 80% to 40-60% without careful optimization. @fig-moe-routing-frontiers visualizes how a routing mechanism (learned gating function) determines which experts process each input, directing tokens to specialized subnetworks based on content type. The router computes probabilities for each expert using learned linear transformations followed by softmax, typically selecting the top-2 experts per token. Load balancing losses ensure uniform expert utilization to prevent collapse to few specialists. This pattern extends naturally to compound systems where different models, tools, or processing pipelines are routed based on input characteristics. -Consider how the routing works in practice, as depicted in @fig-moe-routing-frontiers: when a token like "2+2=" enters the system, the router assigns high weights (0.7) to arithmetic specialists while giving zero weight to vision or language experts. For "Bonjour means", it activates translation experts instead. GPT-4 [@openai2023gpt4] is rumored to use eight expert models of approximately 220B parameters each (unconfirmed by OpenAI), activating only two per token, reducing active computation to 280B parameters while maintaining 1.8T total capacity with 5-7$\times$ inference speedup. +Consider how the routing works in practice, as depicted in @fig-moe-routing-frontiers: when a token like "2+2=" enters the system, the router assigns high weights (0.7) to arithmetic specialists while giving zero weight to vision or language experts. For "Bonjour means", it activates translation experts instead. GPT-4 [@openai2023gpt4] is rumored to use eight expert models of approximately 220B parameters each (unconfirmed by OpenAI), activating only two per token, reducing active computation to 280B parameters while maintaining 1.8T total capacity with 5-7 $\times$ inference speedup. This introduces systems challenges: load balancing across experts, preventing collapse where all routing converges to few experts, and managing irregular memory access patterns. For compound systems, these same challenges apply to routing between different models, databases, and processing pipelines, requiring sophisticated orchestration infrastructure. @@ -1253,7 +1253,7 @@ where x ∈ ℝ is the input token, h ∈ ℝᵈ is the hidden state, y ∈ ℝ The technical breakthrough enabling competitive performance came from selective state spaces where the recurrence parameters themselves depend on the input: Āₜ = f_A(xₜ), B̄ₜ = f_B(xₜ), making the state transition input-dependent rather than fixed. This selectivity allows the model to dynamically adjust which information to remember or forget based on current input content. When processing "The trophy doesn't fit in the suitcase because it's too big," the model can selectively maintain "trophy" in state while discarding less relevant words, with the selection driven by learned input-dependent gating similar to LSTM forget gates but within the state space framework. This approach resembles maintaining a running summary that adapts its compression strategy based on content importance rather than blindly summarizing everything equally. -Models like Mamba [@gu2023mamba], RWKV [@peng2023rwkv], and Liquid Time-constant Networks [@hasani2020liquid] demonstrate that this approach can match transformer performance on many tasks while scaling linearly rather than quadratically with sequence length. Using selective state spaces with input-dependent parameters, Mamba achieves 5$\times$ better throughput on long sequences (100K+ tokens) compared to transformers. Mamba-7B matches transformer-7B performance on text while using 5$\times$ less memory for 100K token sequences. Subsequent developments including Mamba-2 have further improved both efficiency and quality, while hybrid architectures combining state space layers with attention (as in Jamba) suggest that the future may involve complementary mechanisms rather than wholesale architectural replacement. RWKV combines the efficient inference of RNNs with the parallelizable training of transformers, while Liquid Time-constant Networks adapt their dynamics based on input, showing particular promise for time-series and continuous control tasks. +Models like Mamba [@gu2023mamba], RWKV [@peng2023rwkv], and Liquid Time-constant Networks [@hasani2020liquid] demonstrate that this approach can match transformer performance on many tasks while scaling linearly rather than quadratically with sequence length. Using selective state spaces with input-dependent parameters, Mamba achieves 5 $\times$ better throughput on long sequences (100K+ tokens) compared to transformers. Mamba-7B matches transformer-7B performance on text while using 5 $\times$ less memory for 100K token sequences. Subsequent developments including Mamba-2 have further improved both efficiency and quality, while hybrid architectures combining state space layers with attention (as in Jamba) suggest that the future may involve complementary mechanisms rather than wholesale architectural replacement. RWKV combines the efficient inference of RNNs with the parallelizable training of transformers, while Liquid Time-constant Networks adapt their dynamics based on input, showing particular promise for time-series and continuous control tasks. Systems engineering implications are significant. Linear scaling enables processing book-length contexts, multi-hour conversations, or entire codebases within single model calls. This requires rethinking data loading strategies (handling MB-scale inputs), memory management (streaming rather than batch processing), and distributed inference patterns optimized for sequential processing rather than parallel attention. @@ -1429,7 +1429,7 @@ Stage 2 collects comparative feedback to train a reward model. Rather than ratin Stage 3 applies reinforcement learning to optimize policy using the learned reward model. Proximal Policy Optimization (PPO)[^fn-ppo] [@schulman2017proximal] fine-tunes the language model to maximize expected reward while preventing excessive deviation from the supervised fine-tuned initialization through KL divergence penalties. -[^fn-ppo]: **Proximal Policy Optimization (PPO)**: Reinforcement learning algorithm developed by OpenAI (2017) that updates policies incrementally to prevent training instability. Key innovation: clipping the objective function to limit how much the policy can change per update, preventing catastrophic forgetting and reward hacking. PPO became the standard for RLHF because it balances sample efficiency with stability. Alternative approaches like Direct Preference Optimization (DPO) eliminate the RL phase entirely, directly optimizing from preference pairs with 2-3$\times$ lower computational cost. +[^fn-ppo]: **Proximal Policy Optimization (PPO)**: Reinforcement learning algorithm developed by OpenAI (2017) that updates policies incrementally to prevent training instability. Key innovation: clipping the objective function to limit how much the policy can change per update, preventing catastrophic forgetting and reward hacking. PPO became the standard for RLHF because it balances sample efficiency with stability. Alternative approaches like Direct Preference Optimization (DPO) eliminate the RL phase entirely, directly optimizing from preference pairs with 2-3 $\times$ lower computational cost. This constraint proves critical: without it, models exploit reward model weaknesses, generating nonsensical outputs that fool the reward predictor but fail true human judgment. The KL penalty β controls this trade-off, typically set to 0.01-0.1, allowing meaningful improvement while maintaining coherent outputs. @fig-rlhf-pipeline traces this iterative process where each reinforcement learning step generates responses, computes rewards, and updates policy gradients until the model converges on outputs that satisfy both the reward model and the KL constraint. @@ -1726,9 +1726,9 @@ fit=(PM)(C2)(TAM),yshift=0/5mm,xshift=-1mm](BB3){}; Examining the details of @fig-rlhf-pipeline reveals substantial engineering complexity in this three-stage pipeline. Each stage requires distinct infrastructure: Stage 1 needs demonstration collection systems, Stage 2 demands ranking interfaces that present multiple outputs side-by-side, and Stage 3 requires careful hyperparameter tuning to prevent the policy from diverging too far from the original model (the KL penalty shown). The feedback loop at the bottom represents continuous iteration, with models often going through multiple rounds of RLHF, each round requiring fresh human data to prevent overfitting to the reward model. -This approach yields significant improvements: InstructGPT [@ouyang2022training] with 1.3B parameters outperforms GPT-3 with `{python} gpt3_params_label` parameters in human evaluations[^fn-rlhf-impact], demonstrating that alignment matters more than scale for user satisfaction. For ML engineers, this means that investing in alignment infrastructure can be more valuable than scaling compute: a 100$\times$ smaller aligned model outperforms a larger unaligned one. +This approach yields significant improvements: InstructGPT [@ouyang2022training] with 1.3B parameters outperforms GPT-3 with `{python} gpt3_params_label` parameters in human evaluations[^fn-rlhf-impact], demonstrating that alignment matters more than scale for user satisfaction. For ML engineers, this means that investing in alignment infrastructure can be more valuable than scaling compute: a 100 $\times$ smaller aligned model outperforms a larger unaligned one. -[^fn-rlhf-impact]: **RLHF Effectiveness**: InstructGPT (1.3B parameters) was preferred over GPT-3 (`{python} gpt3_params_label` parameters) in 85% of human evaluations despite being 100$\times$ smaller. RLHF training reduced harmful outputs by 90%, hallucinations by 40%, and increased user satisfaction by 72%, demonstrating that alignment matters more than scale for practical performance. +[^fn-rlhf-impact]: **RLHF Effectiveness**: InstructGPT (1.3B parameters) was preferred over GPT-3 (`{python} gpt3_params_label` parameters) in 85% of human evaluations despite being 100 $\times$ smaller. RLHF training reduced harmful outputs by 90%, hallucinations by 40%, and increased user satisfaction by 72%, demonstrating that alignment matters more than scale for practical performance. #### Constitutional AI: Value-Aligned Learning {#sec-agi-systems-constitutional-ai-valuealigned-learning-8d4c} @@ -1948,7 +1948,7 @@ to[out=170,in=200,distance=5](-1.0,0.1)to(0.32,-0.07)to(1.3,0.76); } \node[Box,above=0.4 of C2](B1){Constitutional Principles\\ "Be helpful, harmless, honest"}; \draw[LineA](B1)-|node[above,pos=0.25,text=black]{Against}(C3); -\draw[LineA](RR.south) to[bend left=18] node[above,violet]{Iterate 5$\times$}(IR); +\draw[LineA](RR.south) to[bend left=18] node[above,violet]{Iterate 5 $\times$}(IR); % \node[below=16mm of C2, align=center,BrownLine] {\small Harmful: 100\%}; \node[below=16mm of C3, align=center,BrownLine] {\small Harmful: 40\%}; @@ -2001,21 +2001,21 @@ Training GPT-4 class models already requires the extensive parallelism covered i 3D chip stacking and chiplets build density through vertical integration and modular composition rather than horizontal shrinking. Samsung's 176-layer 3D NAND and AMD's multi-chiplet EPYC processors demonstrate feasibility[^fn-3d-chiplet]. For AGI, this enables mixing specialized processors (matrix units, memory controllers, networking chips) in optimal ratios while managing thermal challenges through advanced cooling. -[^fn-3d-chiplet]: **3D Stacking and Chiplets**: Vertical integration achieves 100$\times$ higher interconnect density than planar designs, but generates 1000 W/cm² heat flux requiring advanced cooling (liquid, microfluidic). AMD's MI300X uses 3D chiplets with 153B transistors. Chiplet architectures mix specialized dies (compute, memory, I/O) while improving yields from 30% to 90%+ vs. monolithic designs. +[^fn-3d-chiplet]: **3D Stacking and Chiplets**: Vertical integration achieves 100 $\times$ higher interconnect density than planar designs, but generates 1000 W/cm² heat flux requiring advanced cooling (liquid, microfluidic). AMD's MI300X uses 3D chiplets with 153B transistors. Chiplet architectures mix specialized dies (compute, memory, I/O) while improving yields from 30% to 90%+ vs. monolithic designs. -Communication and memory bottlenecks require novel solutions through optical interconnects and processing-in-memory architectures. Silicon photonics enables 100 Tbps bandwidth with 10$\times$ lower energy than electrical interconnects, critical when coordinating 100,000+ processors[^fn-optical-pim]. Processing-in-memory reduces data movement energy by 100$\times$ by computing directly where data resides, addressing the memory wall limiting current accelerator efficiency. +Communication and memory bottlenecks require novel solutions through optical interconnects and processing-in-memory architectures. Silicon photonics enables 100 Tbps bandwidth with 10 $\times$ lower energy than electrical interconnects, critical when coordinating 100,000+ processors[^fn-optical-pim]. Processing-in-memory reduces data movement energy by 100 $\times$ by computing directly where data resides, addressing the memory wall limiting current accelerator efficiency. -[^fn-optical-pim]: **Communication and Memory Innovations**: Optical interconnects achieve 100$\times$ bandwidth/energy improvement over electrical for chip-to-chip communication, essential as processor arrays scale. Processing-in-memory (Samsung's HBM-PIM) eliminates data movement—responsible for 90% of energy in memory-bound workloads—by computing directly in memory arrays where parameters reside. +[^fn-optical-pim]: **Communication and Memory Innovations**: Optical interconnects achieve 100 $\times$ bandwidth/energy improvement over electrical for chip-to-chip communication, essential as processor arrays scale. Processing-in-memory (Samsung's HBM-PIM) eliminates data movement—responsible for 90% of energy in memory-bound workloads—by computing directly in memory arrays where parameters reside. -Longer-term pathways emerge through neuromorphic and quantum-hybrid systems. Intel's Loihi [@davies2018loihi] and IBM's TrueNorth demonstrate 1000$\times$ energy efficiency for event-driven workloads through brain-inspired architectures. Quantum-classical hybrids could accelerate combinatorial optimization (neural architecture search, hyperparameter tuning) while classical systems handle gradient computation[^fn-neuromorphic-quantum]. Programming these heterogeneous systems requires sophisticated middleware to decompose AGI workflows across different computational paradigms. +Longer-term pathways emerge through neuromorphic and quantum-hybrid systems. Intel's Loihi [@davies2018loihi] and IBM's TrueNorth demonstrate 1000 $\times$ energy efficiency for event-driven workloads through brain-inspired architectures. Quantum-classical hybrids could accelerate combinatorial optimization (neural architecture search, hyperparameter tuning) while classical systems handle gradient computation[^fn-neuromorphic-quantum]. Programming these heterogeneous systems requires sophisticated middleware to decompose AGI workflows across different computational paradigms. -[^fn-neuromorphic-quantum]: **Alternative Computing Paradigms**: Neuromorphic chips achieve 1000$\times$ energy efficiency for sparse, event-driven workloads but require new programming models. Quantum processors show advantages for specific optimization tasks (IBM's 1000+ qubit systems, Google's Sycamore), though hybrid quantum-classical systems face orchestration challenges due to vastly different computational timescales. +[^fn-neuromorphic-quantum]: **Alternative Computing Paradigms**: Neuromorphic chips achieve 1000 $\times$ energy efficiency for sparse, event-driven workloads but require new programming models. Quantum processors show advantages for specific optimization tasks (IBM's 1000+ qubit systems, Google's Sycamore), though hybrid quantum-classical systems face orchestration challenges due to vastly different computational timescales. Hardware acceleration principles (parallelism, memory hierarchy optimization, specialized compute units) remain foundational. AGI systems extend these through post-Moore's Law innovations while requiring unprecedented orchestration across heterogeneous architectures. #### Post-Silicon Frontiers: Biological and DNA Computing {#sec-emerging-systems-challenges-postsilicon-frontiers-biological-dna-computing-6615} -As we approach the atomic limits of silicon, the sustainability crisis established in @sec-sustainable-ai forces us to look beyond traditional semiconductors. The 1,000,000$\times$ energy efficiency gap between the human brain and current GPUs suggests that silicon may be an inherently suboptimal substrate for general intelligence. +As we approach the atomic limits of silicon, the sustainability crisis established in @sec-sustainable-ai forces us to look beyond traditional semiconductors. The 1,000,000 $\times$ energy efficiency gap between the human brain and current GPUs suggests that silicon may be an inherently suboptimal substrate for general intelligence. - **Biological Computing**: Emerging research explores "Organoid Intelligence"—using living neurons integrated with silicon interfaces to perform recognition and learning tasks. These systems operate at the milliwatt scale, potentially offering a path to AGI that bypasses the "Power Wall" of modern datacenters. - **DNA Computing**: To solve the "Memory Wall," researchers are investigating DNA as a storage and computational substrate. With an information density of 215 petabytes per gram, a single kilogram of DNA could store the entire training corpus of a sentient-level AGI. When combined with molecular-scale parallel processing, DNA computing could enable "Knowledge Tiers" that are billions of times denser than HBM3 or High Bandwidth Flash (HBF). @@ -2110,24 +2110,24 @@ The challenge extends beyond storage to organization and retrieval. Human memory Energy consumption presents quantifiable constraints that bound what scaling can achieve economically. GPT-4 training is estimated to have consumed 50-100 GWh of electricity [@epoch2022compute], enough to power 50,000 homes for a year[^fn-gpt4-energy]. Understanding both thermodynamic limits and practical constraints enables realistic assessment of efficiency improvement potential. -[^fn-gpt4-energy]: **GPT-4 Energy Consumption**: Estimated 50-100 GWh for training, equivalent to 50,000 US homes' annual electricity. At $0.10/kWh plus $100M+ hardware amortization, total training cost exceeds $100M. Inference adds ongoing costs: serving GPT-4 to millions requires 10,000+ GPUs continuously. Scaling 10$\times$ further would require energy comparable to small cities. +[^fn-gpt4-energy]: **GPT-4 Energy Consumption**: Estimated 50-100 GWh for training, equivalent to 50,000 US homes' annual electricity. At $0.10/kWh plus $100M+ hardware amortization, total training cost exceeds $100M. Inference adds ongoing costs: serving GPT-4 to millions requires 10,000+ GPUs continuously. Scaling 10 $\times$ further would require energy comparable to small cities. **Thermodynamic bounds on computation** establish fundamental limits independent of architecture. The Landauer limit[^fn-landauer] sets the minimum energy required to erase one bit of information at temperature T: $E_{min} = k_B T \ln 2 \approx 2.9 \times 10^{-21}$ joules at room temperature (300K). Current transistors operate at approximately $10^{-15}$ joules per operation, roughly 3 million times above the Landauer limit. [^fn-landauer]: **Landauer Limit**: Rolf Landauer (1961) proved that erasing one bit of information necessarily dissipates at least $k_B T \ln 2$ joules of energy, where $k_B$ is Boltzmann's constant. This follows from the second law of thermodynamics: computation that discards information increases entropy, requiring energy dissipation. Reversible computing theoretically avoids this limit by preserving information, but practical reversible computers remain research projects. **Uncertainty caveat**: Whether approaching Landauer efficiency is practically achievable for useful computation remains unknown. -**Practical limits** are much tighter than thermodynamic limits. The human brain operates on 20 watts while performing computations that would require megawatts on current hardware[^fn-brain-efficiency]. This efficiency gap (approximately 360$\times$ by one estimate) emerges from architectural differences: biological neurons operate at approximately 1 Hz effective compute rates using chemical signaling, while digital processors run at GHz frequencies using electronic switching. Despite the frequency disadvantage, the brain's extensive parallelism (10¹¹ neurons with 10¹⁴ connections) and analog processing enable efficient pattern recognition that digital systems achieve only through higher energy expenditure. +**Practical limits** are much tighter than thermodynamic limits. The human brain operates on 20 watts while performing computations that would require megawatts on current hardware[^fn-brain-efficiency]. This efficiency gap (approximately 360 $\times$ by one estimate) emerges from architectural differences: biological neurons operate at approximately 1 Hz effective compute rates using chemical signaling, while digital processors run at GHz frequencies using electronic switching. Despite the frequency disadvantage, the brain's extensive parallelism (10¹¹ neurons with 10¹⁴ connections) and analog processing enable efficient pattern recognition that digital systems achieve only through higher energy expenditure. -[^fn-brain-efficiency]: **Biological vs Digital Efficiency**: Brain: approximately 10¹⁵ ops/sec ÷ 20 W = 5 $\times$ 10¹³ ops/watt [@sandberg2008whole]. H100 GPU: 1.98 $\times$ 10¹⁵ ops/sec ÷ `{python} h100_tdp_w` W = 2.8 $\times$ 10¹² ops/watt. Efficiency ratio: approximately 360$\times$ advantage for biological computation. **Critical caveat**: This comparison requires careful interpretation. Biological neurons use analog, chemical signaling with massive parallelism, while digital systems use precise, electronic switching with sequential processing. The mechanisms are fundamentally different, making direct efficiency comparisons approximate. What computation the brain performs per "operation" is not well defined. +[^fn-brain-efficiency]: **Biological vs Digital Efficiency**: Brain: approximately 10¹⁵ ops/sec ÷ 20 W = 5 $\times$ 10¹³ ops/watt [@sandberg2008whole]. H100 GPU: 1.98 $\times$ 10¹⁵ ops/sec ÷ `{python} h100_tdp_w` W = 2.8 $\times$ 10¹² ops/watt. Efficiency ratio: approximately 360 $\times$ advantage for biological computation. **Critical caveat**: This comparison requires careful interpretation. Biological neurons use analog, chemical signaling with massive parallelism, while digital systems use precise, electronic switching with sequential processing. The mechanisms are fundamentally different, making direct efficiency comparisons approximate. What computation the brain performs per "operation" is not well defined. -**Improvement potential**: Current digital systems have approximately 10⁶$\times$ improvement potential to reach Landauer limits, but each 10$\times$ improvement becomes progressively harder. Practical improvement paths include: +**Improvement potential**: Current digital systems have approximately 10⁶ $\times$ improvement potential to reach Landauer limits, but each 10 $\times$ improvement becomes progressively harder. Practical improvement paths include: -- **Neuromorphic architectures**: Intel's Loihi achieves 1000$\times$ efficiency gains for sparse, event-driven workloads, but this applies to specific workload types +- **Neuromorphic architectures**: Intel's Loihi achieves 1000 $\times$ efficiency gains for sparse, event-driven workloads, but this applies to specific workload types - **Reversible computing**: Theoretical elimination of Landauer costs, but no practical implementations exist - **Cryogenic computing**: Reducing temperature decreases $k_B T$, but cooling costs typically exceed energy savings - **Algorithmic efficiency**: Better algorithms can reduce compute requirements, but fundamental task complexity provides lower bounds -**Quantitative constraint**: Closing the efficiency gap by 1000$\times$ (from current H100 efficiency to brain-like efficiency) would reduce GPT-4 training energy from 50-100 GWh to 50-100 MWh. This represents a meaningful but not transformative change in economic feasibility. Solutions building on @sec-sustainable-ai (neuromorphic architectures, reversible computing, algorithmic improvements) address different aspects of this gap with varying degrees of maturity and applicability. +**Quantitative constraint**: Closing the efficiency gap by 1000 $\times$ (from current H100 efficiency to brain-like efficiency) would reduce GPT-4 training energy from 50-100 GWh to 50-100 MWh. This represents a meaningful but not transformative change in economic feasibility. Solutions building on @sec-sustainable-ai (neuromorphic architectures, reversible computing, algorithmic improvements) address different aspects of this gap with varying degrees of maturity and applicability. ### Causal Reasoning and Planning Capabilities {#sec-agi-systems-causal-reasoning-planning-capabilities-32be} @@ -2386,7 +2386,7 @@ Consensus mechanisms for multi-agent systems face complexity beyond traditional [^fn-agent-consensus]: **Agent Consensus Approaches**: Voting selects majority outputs but fails when outputs are diverse. Ranking uses a separate model to evaluate outputs but adds latency. Orchestrator arbitration uses rules or another model to select outputs. Production systems typically use orchestrator arbitration for simplicity, though voting can improve reliability on tasks with clear correct answers. -Resource coordination across many agents requires scheduling algorithms that consider agent capabilities, current load, and task requirements. Reasoning tasks vary substantially (10-1000$\times$) in compute requirements, making predictive scheduling valuable but difficult[^fn-agent-scheduling]. Current orchestration frameworks (Kubernetes, Ray) provide foundations but lack ML-specific scheduling heuristics. +Resource coordination across many agents requires scheduling algorithms that consider agent capabilities, current load, and task requirements. Reasoning tasks vary substantially (10-1000 $\times$) in compute requirements, making predictive scheduling valuable but difficult[^fn-agent-scheduling]. Current orchestration frameworks (Kubernetes, Ray) provide foundations but lack ML-specific scheduling heuristics. [^fn-agent-scheduling]: **Agent Scheduling Challenges**: Reasoning task compute requirements are difficult to predict before execution. Simple heuristics (round-robin, least-loaded) perform poorly when task variance is high. More sophisticated approaches use task classification or adaptive time limits, but no approach dominates across workloads. @@ -2422,7 +2422,7 @@ Three investment areas have clear, quantifiable returns independent of which cap **Multimodal systems**: Processing text, images, audio, and video jointly provides capabilities unavailable from single-modality systems. The engineering is mature enough for production deployment, but the value depends on specific applications. Investment makes sense for applications with clear multimodal requirements. -**Personalization infrastructure**: Parameter-efficient fine-tuning (reducing costs 1000$\times$) and retrieval systems for personal knowledge bases enable new application categories. The technical foundations are solid, but whether users will adopt personalized AI systems at scale remains uncertain. +**Personalization infrastructure**: Parameter-efficient fine-tuning (reducing costs 1000 $\times$) and retrieval systems for personal knowledge bases enable new application categories. The technical foundations are solid, but whether users will adopt personalized AI systems at scale remains uncertain. ### High-Uncertainty Areas {#sec-emerging-systems-challenges-highuncertainty-areas-46b4} @@ -2436,7 +2436,7 @@ These opportunities require addressing engineering challenges that affect curren #### Reliability at Scale {#sec-emerging-systems-challenges-reliability-scale-8c14} -When training runs cost millions of dollars and involve thousands of components, even 99.9% reliability means frequent failures destroying weeks of progress. This demands checkpointing that restarts from recent states, recovery mechanisms salvaging partial progress, and graceful degradation maintaining quality when components fail. Moving from 99.9% to 99.99% reliability (a 10$\times$ reduction in failure rate) proves disproportionately expensive, requiring redundancy, predictive failure detection, and fault-tolerant algorithms. **This is a current, quantifiable problem with known engineering approaches, not a speculative future challenge.** +When training runs cost millions of dollars and involve thousands of components, even 99.9% reliability means frequent failures destroying weeks of progress. This demands checkpointing that restarts from recent states, recovery mechanisms salvaging partial progress, and graceful degradation maintaining quality when components fail. Moving from 99.9% to 99.99% reliability (a 10 $\times$ reduction in failure rate) proves disproportionately expensive, requiring redundancy, predictive failure detection, and fault-tolerant algorithms. **This is a current, quantifiable problem with known engineering approaches, not a speculative future challenge.** #### Heterogeneous System Orchestration {#sec-emerging-systems-challenges-heterogeneous-system-orchestration-e9a9} @@ -2510,7 +2510,7 @@ Frontier ML systems development combines engineering complexity with uncertain c **Fallacy:** ***Scaling solves all capability limitations.*** -Engineers assume that current AI limitations reflect insufficient model size and that scaling addresses capability gaps. In production, empirical scaling laws show diminishing returns on specific capability measures. While GPT-3's `{python} gpt3_params_label` parameters outperform GPT-2's `{python} gpt2_params_label` on benchmarks, the relationship between scale and specific capabilities (reasoning, factual accuracy, instruction following) is non-linear and task-dependent. Scaling GPT-3 to 17.5T parameters (100$\times$ larger) would require approximately $10B in training costs and consume 5 GWh of electricity, with uncertain capability improvements on tasks where current models struggle. As @sec-emerging-systems-scaling-limitations establishes, scaling provides predictable improvements on test loss but does not guarantee proportional improvements on specific capabilities. **Defensible approach**: Quantify expected capability improvements for specific tasks before committing resources, and hedge against uncertainty by investing in architectural alternatives alongside scaling. +Engineers assume that current AI limitations reflect insufficient model size and that scaling addresses capability gaps. In production, empirical scaling laws show diminishing returns on specific capability measures. While GPT-3's `{python} gpt3_params_label` parameters outperform GPT-2's `{python} gpt2_params_label` on benchmarks, the relationship between scale and specific capabilities (reasoning, factual accuracy, instruction following) is non-linear and task-dependent. Scaling GPT-3 to 17.5T parameters (100 $\times$ larger) would require approximately $10B in training costs and consume 5 GWh of electricity, with uncertain capability improvements on tasks where current models struggle. As @sec-emerging-systems-scaling-limitations establishes, scaling provides predictable improvements on test loss but does not guarantee proportional improvements on specific capabilities. **Defensible approach**: Quantify expected capability improvements for specific tasks before committing resources, and hedge against uncertainty by investing in architectural alternatives alongside scaling. **Fallacy:** ***Compound systems are temporary workarounds.*** @@ -2522,22 +2522,22 @@ Engineers assume that advanced ML systems necessitate abandoning existing practi **Pitfall:** ***Treating biological efficiency as achievable target.*** -Teams assume that biological neural efficiency (20W for brain-scale computation) represents an achievable target for silicon systems. In production, biological and silicon substrates operate on fundamentally different physics with different strengths. Digital systems excel at precise arithmetic, reliable storage, and rapid communication; biological neurons achieve analog computation, massive parallelism, and low-power operation. Neuromorphic chips achieve 1000$\times$ efficiency gains for specific workloads (event-driven perception) but struggle with dense matrix operations where GPUs excel [@davies2018loihi]. **Defensible approach**: Use biological efficiency as a theoretical bound rather than engineering target; focus on practical efficiency improvements (1000$\times$ is achievable; 10⁶$\times$ is theoretical). +Teams assume that biological neural efficiency (20W for brain-scale computation) represents an achievable target for silicon systems. In production, biological and silicon substrates operate on fundamentally different physics with different strengths. Digital systems excel at precise arithmetic, reliable storage, and rapid communication; biological neurons achieve analog computation, massive parallelism, and low-power operation. Neuromorphic chips achieve 1000 $\times$ efficiency gains for specific workloads (event-driven perception) but struggle with dense matrix operations where GPUs excel [@davies2018loihi]. **Defensible approach**: Use biological efficiency as a theoretical bound rather than engineering target; focus on practical efficiency improvements (1000 $\times$ is achievable; 10⁶ $\times$ is theoretical). **Fallacy:** ***Inference-time compute makes training efficiency irrelevant.*** -Engineers assume that inference-time scaling (as demonstrated by o1 and o3 models) shifts the bottleneck entirely from training to serving. In production, inference-time compute multiplies serving costs by 10-100$\times$ per query. A reasoning model performing 1000 inference steps per query consumes 1000$\times$ more compute than single-pass generation. Training efficiency determines the base capability from which reasoning begins; better base models require less inference-time compute for equivalent performance. **Defensible approach**: Optimize both training and inference efficiency; the cost-optimal balance depends on query volume and task complexity. +Engineers assume that inference-time scaling (as demonstrated by o1 and o3 models) shifts the bottleneck entirely from training to serving. In production, inference-time compute multiplies serving costs by 10-100 $\times$ per query. A reasoning model performing 1000 inference steps per query consumes 1000 $\times$ more compute than single-pass generation. Training efficiency determines the base capability from which reasoning begins; better base models require less inference-time compute for equivalent performance. **Defensible approach**: Optimize both training and inference efficiency; the cost-optimal balance depends on query volume and task complexity. **Pitfall:** ***Planning based on extrapolated scaling laws.*** -Teams commit multi-year roadmaps assuming scaling laws predict capability trajectories. In production, scaling laws describe test loss within tested regimes but provide uncertain guidance for capability prediction. Historical AI predictions show systematic overconfidence: expert surveys show median prediction errors of 10-100$\times$ on 5-10 year timescales [@grace2018will]. Scaling from GPT-3 to GPT-4 yielded capability improvements that loss metrics did not predict. **Defensible approach**: Plan for uncertainty through hedging and staged investment; avoid commitments that depend on specific capability timelines. +Teams commit multi-year roadmaps assuming scaling laws predict capability trajectories. In production, scaling laws describe test loss within tested regimes but provide uncertain guidance for capability prediction. Historical AI predictions show systematic overconfidence: expert surveys show median prediction errors of 10-100 $\times$ on 5-10 year timescales [@grace2018will]. Scaling from GPT-3 to GPT-4 yielded capability improvements that loss metrics did not predict. **Defensible approach**: Plan for uncertainty through hedging and staged investment; avoid commitments that depend on specific capability timelines. ### Emerging Hardware for LLMs {#sec-emerging-systems-challenges-emerging-hardware-llms-3bf7} The memory wall and interconnect bottlenecks examined throughout this book have driven architectural innovation beyond the standard GPU/TPU paradigms. As established in @sec-compute, traditional accelerators are often memory-capacity constrained for inference-heavy workloads (like serving 100B+ parameter models) and bandwidth-constrained for the autoregressive decode phase. Recent research identifies four architectural opportunities specifically targeting the distinct requirements of Large Language Model (LLM) inference [@ma2024challenges]. #### High Bandwidth Flash (HBF) {#sec-emerging-systems-challenges-high-bandwidth-flash-hbf-2f2f} -Standard SSDs offer vast capacity but insufficient bandwidth for direct model serving, while HBM offers extreme bandwidth but at high cost and limited capacity. High Bandwidth Flash (HBF) proposes a middle tier: integrating flash memory with high-speed interfaces to serve as active memory for "cold" or "warm" model weights. By targeting bandwidths of 50-100 GB/s per device (10$\times$ standard SSDs), HBF systems could host trillion-parameter models on a single node, replacing the need for massive GPU clusters for inference-only workloads. This approach trades latency for capacity, making it ideal for the *prefill* phase or for routing-based Mixture of Experts (MoE) models where only a fraction of parameters are active per token. +Standard SSDs offer vast capacity but insufficient bandwidth for direct model serving, while HBM offers extreme bandwidth but at high cost and limited capacity. High Bandwidth Flash (HBF) proposes a middle tier: integrating flash memory with high-speed interfaces to serve as active memory for "cold" or "warm" model weights. By targeting bandwidths of 50-100 GB/s per device (10 $\times$ standard SSDs), HBF systems could host trillion-parameter models on a single node, replacing the need for massive GPU clusters for inference-only workloads. This approach trades latency for capacity, making it ideal for the *prefill* phase or for routing-based Mixture of Experts (MoE) models where only a fraction of parameters are active per token. #### Processing-Near-Memory (PNM) {#sec-emerging-systems-challenges-processingnearmemory-pnm-c02b} Processing-in-Memory (PIM) has long promised to reduce data movement by computing inside DRAM banks. However, adoption has been slow due to manufacturing complexity and limited logic performance. Processing-Near-Memory (PNM) offers a pragmatic alternative: placing logic logic dies extremely close to memory dies (e.g., via 3D stacking or silicon interposers) without modifying the DRAM process itself. This approach specifically benefits the *decode* phase of LLM inference, which is almost entirely memory-bandwidth bound. By minimizing the physical distance and energy cost of moving weights to compute units, PNM architectures can achieve significantly higher tokens-per-second per watt than general-purpose GPUs. @@ -2561,7 +2561,7 @@ The central insight is that **engineering trade-offs, not capability predictions * **Scaling Has Limits**: Scaling laws describe test loss, not capabilities. Data exhaustion, diminishing returns, and energy costs create constraints that purely scaling-based strategies face. Chinchilla optimal scaling establishes that data quality often matters more than model size. * **Compound Systems Are Engineering Solutions**: Integrating specialized components (retrieval, code execution, verification) addresses current limitations with measurable improvements. This is an engineering pattern, not a pathway to qualitatively different capabilities. * **Uncertainty Requires Hedging**: Historical AI predictions show systematic overconfidence. Defensible infrastructure investments focus on capabilities with quantifiable returns (efficiency, reliability, orchestration) rather than betting on specific capability trajectories. -* **Thermodynamic Bounds Matter**: The Landauer limit establishes fundamental energy constraints. Current systems operate 10⁶$\times$ above this limit, suggesting improvement potential but also practical constraints on achieving brain-like efficiency. +* **Thermodynamic Bounds Matter**: The Landauer limit establishes fundamental energy constraints. Current systems operate 10⁶ $\times$ above this limit, suggesting improvement potential but also practical constraints on achieving brain-like efficiency. * **Systems Engineering Remains Essential**: Distributed training, efficient serving, robust operations, and responsible governance provide value regardless of which architectural approaches prove most effective. These are high-confidence investments. ::: diff --git a/book/quarto/contents/vol2/fault_tolerance/fault_tolerance.qmd b/book/quarto/contents/vol2/fault_tolerance/fault_tolerance.qmd index c1e4273577..5179fd4432 100644 --- a/book/quarto/contents/vol2/fault_tolerance/fault_tolerance.qmd +++ b/book/quarto/contents/vol2/fault_tolerance/fault_tolerance.qmd @@ -142,7 +142,7 @@ Consider training a **Llama-3 (Lighthouse)** 70B model on 1,024 A100 GPUs. * **GPU Logic**: 4,000 FIT per GPU $\times$ 1,024 = 4,096,000 FIT $\rightarrow$ **MTBF $\approx$ 244 hours**. * **HBM2 Memory**: 250 FIT per Mb. Total Mb = $1,024 \times 80 \text{ GB} \times 8 \times 1,024 \approx 671 \text{M Mb}$. Total FIT $\approx$ 167B FIT $\rightarrow$ **MTBF $\approx$ 21 seconds**. -Without ECC memory protection, large-scale training is impossible as corruption would occur every 21 seconds. With ECC providing a 100$\times$ reduction in soft error rates, the memory MTBF improves to 36 minutes, and the combined system MTBF becomes approximately **35 minutes**. This quantitative analysis justifies checkpoint frequencies of 15-30 minutes for large-scale training. +Without ECC memory protection, large-scale training is impossible as corruption would occur every 21 seconds. With ECC providing a 100 $\times$ reduction in soft error rates, the memory MTBF improves to 36 minutes, and the combined system MTBF becomes approximately **35 minutes**. This quantitative analysis justifies checkpoint frequencies of 15-30 minutes for large-scale training. ### Worked Example: Cluster MTBF Calculation {#sec-fault-tolerance-reliability-reliability-worked-example-cluster-mtbf-calculation-9255} @@ -778,8 +778,8 @@ This result demonstrates why failure analysis matters: without knowing the syste The Young-Daly formula provides valuable intuition but rests on assumptions that may not hold in practice: 1. **Exponentially distributed failures**: Assumes constant failure rate. Real systems exhibit "bathtub curve" behavior with higher rates during burn-in and wear-out phases. -2. **Deterministic checkpoint time**: Assumes $T_{save}$ is constant. In practice, checkpoint time varies 2–3$\times$ due to storage contention, network congestion, and memory pressure. -3. **Recovery time equals checkpoint time**: Assumes recovery reads same data written during checkpoint. Often recovery takes 3–5$\times$ longer due to job scheduling delays, topology reconstruction, and warmup. +2. **Deterministic checkpoint time**: Assumes $T_{save}$ is constant. In practice, checkpoint time varies 2–3 $\times$ due to storage contention, network congestion, and memory pressure. +3. **Recovery time equals checkpoint time**: Assumes recovery reads same data written during checkpoint. Often recovery takes 3–5 $\times$ longer due to job scheduling delays, topology reconstruction, and warmup. 4. **Single failure mode**: Assumes one failure at a time. Correlated failures (power, cooling, shared switch) violate this assumption. 5. **Infinite timeline**: Optimal for long training runs. Short runs (where total time is comparable to MTBF) require different analysis. @@ -1020,7 +1020,7 @@ Container orchestration systems and cluster managers provide health check mechan **Loss Spike Detection** -Silent hardware errors often fail to crash the process but corrupt the mathematical result. Bit flips in ALU logic exemplify this failure mode. In training, this manifests as a sudden, catastrophic spike in the loss function. Loss jumps 10x-100$\times$ or becomes NaN or infinity instantly. This is rarely an algorithmic issue if hyperparameters were stable. It is a signature of hardware failure. Systems should automatically pause training, identify which rank contributed the corrupted gradient, and drain that node before restarting from the last good checkpoint. Checksums or replay can identify the culprit rank. +Silent hardware errors often fail to crash the process but corrupt the mathematical result. Bit flips in ALU logic exemplify this failure mode. In training, this manifests as a sudden, catastrophic spike in the loss function. Loss jumps 10x-100 $\times$ or becomes NaN or infinity instantly. This is rarely an algorithmic issue if hyperparameters were stable. It is a signature of hardware failure. Systems should automatically pause training, identify which rank contributed the corrupted gradient, and drain that node before restarting from the last good checkpoint. Checksums or replay can identify the culprit rank. **Training dynamics monitoring**: Monitoring loss values, gradient norms, and other training statistics can detect Byzantine failures that do not cause explicit errors. Sudden loss spikes or gradient explosions may indicate silent corruption. @@ -1870,7 +1870,7 @@ Organizations commonly set checkpoint intervals based on "feels right": "every h $$T_{opt} = \sqrt{2 \times 5 \times 240} = \sqrt{2400} \approx 49 \text{ minutes}$$ -The intuitive "every hour" is close but suboptimal. More critically, if checkpoint time increases to 15 minutes (larger model, slower storage), the optimal interval becomes 85 minutes, not the "every 15 minutes" that some teams adopt to "stay safe." Too-frequent checkpointing wastes more compute than it saves. The quantitative approach reveals that intuition-based intervals often deviate 2–3$\times$ from optimal in either direction. +The intuitive "every hour" is close but suboptimal. More critically, if checkpoint time increases to 15 minutes (larger model, slower storage), the optimal interval becomes 85 minutes, not the "every 15 minutes" that some teams adopt to "stay safe." Too-frequent checkpointing wastes more compute than it saves. The quantitative approach reveals that intuition-based intervals often deviate 2–3 $\times$ from optimal in either direction. **Fallacy: MTBF assumes independent failures.** @@ -1897,7 +1897,7 @@ While the Young-Daly formula accounts for checkpoint save time, practitioners of 4. **Communication re-establishment**: NCCL ring topology reconstruction -Total restart time can be 3–5$\times$ checkpoint save time. A 5-minute checkpoint save followed by 20-minute restart means effective failure cost is 25 minutes, not 5 minutes. The modified Young-Daly formula should use $T_{save} + T_{restart}$ for the overhead term, significantly increasing optimal checkpoint intervals. +Total restart time can be 3–5 $\times$ checkpoint save time. A 5-minute checkpoint save followed by 20-minute restart means effective failure cost is 25 minutes, not 5 minutes. The modified Young-Daly formula should use $T_{save} + T_{restart}$ for the overhead term, significantly increasing optimal checkpoint intervals. **Pitfall: Treating all failures as "restarts".** diff --git a/book/quarto/contents/vol2/inference/inference.qmd b/book/quarto/contents/vol2/inference/inference.qmd index 14ef128b81..35191064eb 100644 --- a/book/quarto/contents/vol2/inference/inference.qmd +++ b/book/quarto/contents/vol2/inference/inference.qmd @@ -177,7 +177,7 @@ where $C_{training}$ is the one-time cost to train the model, $C_{serving}$ is t **Training Cost ($C_{train}$)** - Hardware: 1,000 GPU-hours @ $3/hr = $3,000 -- Engineering Overhead (Data/Exp): 3$\times$ Hardware = $9,000 +- Engineering Overhead (Data/Exp): 3 $\times$ Hardware = $9,000 - **Total Training**: $12,000 **Serving Cost ($C_{serve}$)** @@ -194,7 +194,7 @@ $$ C_{serve\_total} = 6.31 \times 10^{11} \times \$10^{-5} = \mathbf{\$6,310,000 **Conclusion**: $$ \text{Ratio} = \frac{\$6,310,000}{\$12,000} \approx \mathbf{526\times} $$ -Serving optimization leverage is 500$\times$ higher than training optimization. A 1% serving efficiency gain saves \$63k—5$\times$ the entire training budget. +Serving optimization leverage is 500 $\times$ higher than training optimization. A 1% serving efficiency gain saves \$63k—5 $\times$ the entire training budget. ::: The cost dominance ratio varies by application. @tbl-cost-ratios quantifies this disparity: @@ -349,7 +349,7 @@ Selection depends on model type, deployment constraints, and organizational fact vLLM[^fn-vllm-name] [@kwon2023vllm] has emerged as the leading open-source LLM serving framework due to its PagedAttention innovation. Understanding its architecture illustrates key LLM serving principles. -[^fn-vllm-name]: **vLLM**: The name stands for "virtual LLM," reflecting its core innovation of applying virtual memory concepts to KV cache management. Developed at UC Berkeley and released in 2023, vLLM achieved 24$\times$ throughput improvement over HuggingFace Transformers in initial benchmarks. +[^fn-vllm-name]: **vLLM**: The name stands for "virtual LLM," reflecting its core innovation of applying virtual memory concepts to KV cache management. Developed at UC Berkeley and released in 2023, vLLM achieved 24 $\times$ throughput improvement over HuggingFace Transformers in initial benchmarks. The core innovations in vLLM include PagedAttention (virtual memory for KV cache, covered in @sec-inference-scale-pagedattention-3c94), continuous batching (add/remove requests mid-generation), optimized attention kernels (FlashAttention [@dao2022flashattention] integration), and tensor parallelism (automatic model sharding across GPUs). @@ -1281,7 +1281,7 @@ Continuous batching's dynamic batch management maintains high GPU utilization re $$\text{Throughput gain} \approx 1 + \frac{CV^2}{2}$$ {#eq-continuous-batching-gain} -With typical LLM output lengths having $CV \approx 1.0$, continuous batching achieves approximately 1.5$\times$ throughput improvement. For highly variable outputs (conversational vs. code generation), gains can reach 2–4x. +With typical LLM output lengths having $CV \approx 1.0$, continuous batching achieves approximately 1.5 $\times$ throughput improvement. For highly variable outputs (conversational vs. code generation), gains can reach 2–4x. These analytical gains translate directly into production systems. The following implementation study examines how *vLLM* realizes continuous batching through iteration-level scheduling, paged memory management, and preemption. @@ -1303,7 +1303,7 @@ vLLM implements a virtual memory mechanism similar to an operating system's swap | **Dynamic (timeout=50ms)** | 580 | 65% | | **Continuous** | 1,200 | 92% | -The 3$\times$ throughput improvement from continuous batching comes from eliminating idle GPU cycles during sequence length variation. +The 3 $\times$ throughput improvement from continuous batching comes from eliminating idle GPU cycles during sequence length variation. ::: @@ -1599,7 +1599,7 @@ Continuous batching's performance benefits come with implementation complexity t | **Memory overhead** | Fixed allocation | Dynamic + fragmentation mgmt | | **Scheduler latency** | ~0.1 ms per batch | ~0.5-1 ms per iteration | | **Debugging complexity** | Deterministic behavior | State-dependent, harder to trace | -| **Throughput (variable)** | Baseline | 1.5-3.5$\times$ improvement | +| **Throughput (variable)** | Baseline | 1.5-3.5 $\times$ improvement | | **Throughput (uniform)** | Baseline | ~1.0x (no improvement) | : **Traditional vs Continuous Batching Trade-offs**. Continuous batching provides significant throughput gains for variable-length workloads at the cost of implementation complexity. For uniform-length workloads, the complexity overhead may not justify adoption. {#tbl-batching-tradeoffs} @@ -2023,7 +2023,7 @@ PagedAttention provides several benefits. It eliminates internal fragmentation b **Memory layout**: ``` -Physical blocks (16 tokens × hidden_dim × 2 × precision): +Physical blocks (16 tokens $\times$ hidden_dim $\times$ 2 $\times$ precision): Block 0: [K₀...K₁₅, V₀...V₁₅] Block 1: [K₀...K₁₅, V₀...V₁₅] ... @@ -2071,7 +2071,7 @@ The gather operations add overhead, but it is minimal compared to the memory sav | **Contiguous** | 30-40% | 1.0x (baseline) | | **PagedAttention** | 95%+ | 2.5-4x | -The 2.5-4$\times$ throughput improvement comes from fitting more concurrent sequences in the same memory. +The 2.5-4 $\times$ throughput improvement comes from fitting more concurrent sequences in the same memory. ::: @@ -2261,7 +2261,7 @@ Trade-off: Cannot attend to very old tokens, but sufficient for most tasks. **Grouped-query attention (GQA)**[^fn-gqa] [@ainslie2023gqa]: Architectural change that reduces KV cache by sharing key-value heads: -[^fn-gqa]: **GQA trade-off**: Grouped-query attention interpolates between multi-head attention (MHA, one KV head per query head) and multi-query attention (MQA, one KV head for all query heads). GQA with 8 KV heads for 64 query heads achieves 8$\times$ KV cache reduction versus MHA while maintaining most of MHA's model quality, unlike MQA which can degrade quality significantly. +[^fn-gqa]: **GQA trade-off**: Grouped-query attention interpolates between multi-head attention (MHA, one KV head per query head) and multi-query attention (MQA, one KV head for all query heads). GQA with 8 KV heads for 64 query heads achieves 8 $\times$ KV cache reduction versus MHA while maintaining most of MHA's model quality, unlike MQA which can degrade quality significantly. | **Attention Type** | **KV Heads** | **Cache Size (relative)** | |:------------------------|-------------:|--------------------------:| @@ -2363,12 +2363,12 @@ spec_speedup_str = f"{spec_speedup:.2f}" **Factors affecting speedup**: -| **Acceptance Rate** | **Speedup** | -|:--------------------|-------------------------:| -| **90% (easy text)** | 2.5–3$\times$ | -| **70% (typical)** | 1.5–2$\times$ | -| **50% (hard text)** | 1.2–1.5$\times$ | -| **30% (very hard)** | <1$\times$ (overhead) | +| **Acceptance Rate** | **Speedup** | +|:--------------------|--------------------------:| +| **90% (easy text)** | 2.5–3 $\times$ | +| **70% (typical)** | 1.5–2 $\times$ | +| **50% (hard text)** | 1.2–1.5 $\times$ | +| **30% (very hard)** | <1 $\times$ (overhead) | Speedup depends on how well the draft model predicts the target model's output. For well-aligned model pairs (same training data, similar architecture), acceptance rates of 70-80% are common. @@ -2426,9 +2426,9 @@ Effective KV cache management combines multiple techniques: ``` Available GPU memory = Total - Weights - Activations - Overhead -KV pool size = 0.9 × Available # Leave 10% headroom +KV pool size = 0.9 $\times$ Available # Leave 10% headroom -Max concurrent sequences = KV pool size / (avg_seq_length × per_token_cache) +Max concurrent sequences = KV pool size / (avg_seq_length $\times$ per_token_cache) ``` When cache is full, systems use several eviction policies. LRU (Least Recently Used) evicts sequences with oldest last access. Size-based eviction removes longest sequences first to free most memory. Priority-based eviction protects high-priority or paid-tier requests. @@ -2975,13 +2975,13 @@ Production systems often combine multiple sharding strategies to handle differen **Tensor + Pipeline parallelism**: For very large models that require both memory distribution and latency reduction: ``` -8 GPUs organized as 2 pipeline stages × 4 tensor parallel: +8 GPUs organized as 2 pipeline stages $\times$ 4 tensor parallel: Stage 0 (Layers 1-40): TP across GPUs 0,1,2,3 Stage 1 (Layers 41-80): TP across GPUs 4,5,6,7 ``` -This achieves 4$\times$ latency reduction (from TP) while handling models requiring 8-way sharding for memory. +This achieves 4 $\times$ latency reduction (from TP) while handling models requiring 8-way sharding for memory. **Expert + Tensor parallelism**: For MoE models where individual experts are large: @@ -3054,7 +3054,7 @@ NVLink enables efficient tensor parallelism within a node. Cross-node tensor par NVLink bandwidth has evolved significantly over GPU generations.[^fn-nvlink] -[^fn-nvlink]: **NVLink evolution**: NVLink bandwidth has grown from 160 GB/s (Pascal, 2016) to `{python} nvlink_h100_gbs` GB/s bidirectional (Hopper, 2022) to 1.8 TB/s (Blackwell, 2024). This 10$\times$ improvement enables tensor parallelism across 8+ GPUs with less than 5% communication overhead, making large model inference practical. +[^fn-nvlink]: **NVLink evolution**: NVLink bandwidth has grown from 160 GB/s (Pascal, 2016) to `{python} nvlink_h100_gbs` GB/s bidirectional (Hopper, 2022) to 1.8 TB/s (Blackwell, 2024). This 10 $\times$ improvement enables tensor parallelism across 8+ GPUs with less than 5% communication overhead, making large model inference practical. ### Sharding Strategy Selection {#sec-inference-scale-sharding-strategy-selection-f69c} @@ -3099,7 +3099,7 @@ Round-robin assigns requests in circular order. Request 1 goes to server 1, requ Random assignment selects a server uniformly at random for each request. With large numbers of requests, this converges to even distribution but with higher variance than round-robin. -For homogeneous servers with identical service times, both achieve near-optimal load distribution. However, production systems rarely meet these assumptions. Heterogeneous hardware introduces different GPU generations and memory configurations. Variable request sizes mean some requests take 10$\times$ longer than others. Server state variations occur when some replicas are warming up while others approach memory limits. +For homogeneous servers with identical service times, both achieve near-optimal load distribution. However, production systems rarely meet these assumptions. Heterogeneous hardware introduces different GPU generations and memory configurations. Variable request sizes mean some requests take 10 $\times$ longer than others. Server state variations occur when some replicas are warming up while others approach memory limits. Under these realistic conditions, uninformed strategies perform poorly. The maximum queue length under random assignment follows @eq-random-max-queue[^fn-theta-notation]: @@ -3209,7 +3209,7 @@ Consider a cluster with mixed GPU types: - 10 H100 GPUs (capacity: 1000 QPS each) - 20 A100 GPUs (capacity: 600 QPS each) -- Total capacity: 10$\times$1000 + 20$\times$600 = `{python} total_cap_str` QPS +- Total capacity: 10 $\times$ 1000 + 20 $\times$ 600 = `{python} total_cap_str` QPS **Target traffic**: 15,000 QPS @@ -3565,12 +3565,12 @@ Consider a scenario where one server becomes slow (thermal throttling): **Configuration for GPU inference**: -| **Parameter** | **Value** | **Rationale** | -|:-----------------------|-------------------:|:--------------------------------------| -| **Error threshold** | 30% | GPU OOM failures are serious | -| **Latency threshold** | 2$\times$ baseline | Detect throttling early | -| **Open duration** | 60s | GPU recovery takes time | -| **Half-open requests** | 5 | Careful testing before full reopening | +| **Parameter** | **Value** | **Rationale** | +|:-----------------------|--------------------:|:--------------------------------------| +| **Error threshold** | 30% | GPU OOM failures are serious | +| **Latency threshold** | 2 $\times$ baseline | Detect throttling early | +| **Open duration** | 60s | GPU recovery takes time | +| **Half-open requests** | 5 | Careful testing before full reopening | ::: @@ -4033,7 +4033,7 @@ $$T_{cold start} = T_{provision} + T_{load} + T_{warmup}$$ {#eq-cold-start-time} **Provisioning time** ($T_{provision}$): Acquiring a GPU instance takes 30 seconds to several minutes depending on cloud provider and GPU type.[^fn-cold-start] -[^fn-cold-start]: **Cold start in ML vs. serverless**: The cold start problem for GPU inference is 10-100$\times$ worse than for serverless functions. Lambda functions cold start in 100ms-1s; GPU inference cold starts in 1-10 minutes due to GPU allocation, model loading, and CUDA initialization. This fundamental difference makes predictive scaling and warm pools essential for GPU workloads. +[^fn-cold-start]: **Cold start in ML vs. serverless**: The cold start problem for GPU inference is 10-100 $\times$ worse than for serverless functions. Lambda functions cold start in 100ms-1s; GPU inference cold starts in 1-10 minutes due to GPU allocation, model loading, and CUDA initialization. This fundamental difference makes predictive scaling and warm pools essential for GPU workloads. **Model loading time** ($T_{load}$): Loading model weights from storage to GPU memory. For large models: @@ -4291,7 +4291,7 @@ Maintaining a pool of pre-warmed replicas reduces effective cold start time: $$\text{Warm pool size} = \frac{\text{Max expected spike}}{\text{Per-replica throughput}} \times \text{Headroom factor}$$ -For example, if max spike is 2$\times$ normal and headroom factor is 1.5: +For example, if max spike is 2 $\times$ normal and headroom factor is 1.5: $$\text{Warm pool} = 2 \times 1.5 = 3\text{x minimum pool capacity}$$ @@ -4560,12 +4560,12 @@ Action: Rollback US-East to v2.0 **Metrics for global deployment health**: -| **Metric** | **Per-Region** | **Global** | -|:------------------|:---------------|:-----------------------------| -| **Error rate** | < 0.1% | < 0.1% | -| **P99 latency** | < target | < 2$\times$ single-region | -| **Throughput** | Stable | Stable | -| **Model quality** | Within bounds | Consistent across regions | +| **Metric** | **Per-Region** | **Global** | +|:------------------|:---------------|:------------------------------| +| **Error rate** | < 0.1% | < 0.1% | +| **P99 latency** | < target | < 2 $\times$ single-region | +| **Throughput** | Stable | Stable | +| **Model quality** | Within bounds | Consistent across regions | ### Cost Optimization Across Regions {#sec-inference-scale-cost-optimization-across-regions-c1c2} @@ -4599,7 +4599,7 @@ LLM inference consists of two distinct phases with different computational chara **Prefill phase**: Process the entire input prompt in parallel. Computation scales with prompt length. Memory access pattern is compute-bound (high arithmetic intensity[^fn-arithmetic-intensity]). -[^fn-arithmetic-intensity]: **Arithmetic intensity**: The ratio of compute operations to memory accesses, measured in FLOPs per byte. Prefill achieves 100+ FLOPs/byte (compute-bound), while decode achieves 1-10 FLOPs/byte (memory-bound). This 10-100$\times$ difference explains why prefill and decode require fundamentally different optimization strategies. +[^fn-arithmetic-intensity]: **Arithmetic intensity**: The ratio of compute operations to memory accesses, measured in FLOPs per byte. Prefill achieves 100+ FLOPs/byte (compute-bound), while decode achieves 1-10 FLOPs/byte (memory-bound). This 10-100 $\times$ difference explains why prefill and decode require fundamentally different optimization strategies. **Decode phase**: Generate output tokens one at a time. Each token requires loading entire model weights. Memory access pattern is bandwidth-bound (low arithmetic intensity). @@ -4659,7 +4659,7 @@ The KV cache techniques we just examined address dynamic memory allocation durin This section assumes familiarity with quantization fundamentals including **post-training quantization (PTQ)**, which quantizes a trained model without retraining, and **quantization-aware training (QAT)**, which incorporates quantization effects during training for higher accuracy. Readers should also understand precision formats (FP32, FP16, INT8) and the basic trade-off between numerical precision and memory/compute efficiency. These concepts are covered in standard deep learning optimization references and in Volume I's treatment of model compression. ::: -Quantization reduces numerical precision of model weights and activations, decreasing memory footprint by 2-4$\times$ while increasing decode throughput, which is memory-bandwidth limited rather than compute limited. While these quantization fundamentals are established techniques, serving at scale introduces distinct challenges. Models must be quantized after training without access to training data. Quantization must preserve quality across diverse inputs. Hardware deployment targets vary from datacenter GPUs to edge accelerators. This section examines quantization techniques specifically designed for production inference. +Quantization reduces numerical precision of model weights and activations, decreasing memory footprint by 2-4 $\times$ while increasing decode throughput, which is memory-bandwidth limited rather than compute limited. While these quantization fundamentals are established techniques, serving at scale introduces distinct challenges. Models must be quantized after training without access to training data. Quantization must preserve quality across diverse inputs. Hardware deployment targets vary from datacenter GPUs to edge accelerators. This section examines quantization techniques specifically designed for production inference. ### LLM-Specific Quantization Challenges {#sec-inference-scale-llmspecific-quantization-challenges-b7a2} @@ -4691,10 +4691,10 @@ For each layer's weight matrix W: 1. Compute Hessian: H = X^T X from calibration activations 2. Apply Cholesky factorization to H^{-1} 3. For each column q = 1 to d_col: - a. Quantize column: w_q = round(W[:,q] / Δ) × Δ + a. Quantize column: w_q = round(W[:,q] / Δ) $\times$ Δ b. Compute quantization error: δ = W[:,q] - w_q c. Update remaining columns to compensate: - W[:,q+1:] += δ × [H^{-1}][:,q+1:] / [H^{-1}]_{qq} + W[:,q+1:] += δ $\times$ [H^{-1}][:,q+1:] / [H^{-1}]_{qq} ``` The compensation step propagates quantization error to unquantized columns, where the Hessian-weighted update minimizes output deviation. This achieves $O(d_{row} \cdot d_{col}^2)$ complexity versus $O(d_{row} \cdot d_{col}^3)$ for naive OBS. @@ -4772,7 +4772,7 @@ $$s_j = \max(|X_j|)^\alpha / \max(|W_j|)^{1-\alpha}$$ | **W8A16 (weights only)** | 2x | 1.3x | 1.8x | <0.5% loss | | **W8A8 (SmoothQuant)** | 2x | 1.8-2x | 1.3-1.5x | <1% loss | -**Critical distinction**: W8A8 provides near 2$\times$ speedup for compute-bound prefill (large batch processing initial prompt), but only 1.3-1.5$\times$ speedup for memory-bound decode (generating tokens one at a time). LLM serving is typically decode-heavy, so real-world throughput improvements from W8A8 are often 1.3-1.7$\times$ rather than the theoretical 2$\times$ compute throughput of INT8 Tensor Cores. +**Critical distinction**: W8A8 provides near 2 $\times$ speedup for compute-bound prefill (large batch processing initial prompt), but only 1.3-1.5 $\times$ speedup for memory-bound decode (generating tokens one at a time). LLM serving is typically decode-heavy, so real-world throughput improvements from W8A8 are often 1.3-1.7 $\times$ rather than the theoretical 2 $\times$ compute throughput of INT8 Tensor Cores. ### Rotation-Based Quantization {#sec-inference-scale-rotationbased-quantization-8bee} @@ -4815,7 +4815,7 @@ After transformation, no single value dominates, enabling uniform quantization. | **GPTQ** | W4A16 | 3.24 | +0.12 | | **QuaRot** | W4A4 | 3.31 | +0.19 | -QuaRot achieves 4-bit weights AND 4-bit activations with quality competitive to GPTQ's 4-bit weights only. This enables approximately 8$\times$ memory reduction versus FP16. +QuaRot achieves 4-bit weights AND 4-bit activations with quality competitive to GPTQ's 4-bit weights only. This enables approximately 8 $\times$ memory reduction versus FP16. **SpinQuant**: An extension of QuaRot that learns optimal rotation matrices during a short fine-tuning phase, improving quality at the cost of training compute. @@ -4837,7 +4837,7 @@ Quantization strategies must match target hardware capabilities. Different accel $$\text{Decode throughput} \propto \frac{\text{Memory bandwidth}}{\text{Model size in bytes}}$$ -4-bit quantization delivers 4$\times$ throughput improvement for memory-bound decode, making it highly valuable despite modest compute gains. +4-bit quantization delivers 4 $\times$ throughput improvement for memory-bound decode, making it highly valuable despite modest compute gains. **Deployment configurations**: @@ -4924,7 +4924,7 @@ Choosing the appropriate quantization method depends on deployment constraints: | **AWQ 4-bit on 4xA100** | $1.20 | | **AWQ 4-bit on 2xA100** | $0.60 | -Quantization can reduce serving costs by 2-4$\times$ while maintaining acceptable quality, making it essential for cost-effective LLM deployment. +Quantization can reduce serving costs by 2-4 $\times$ while maintaining acceptable quality, making it essential for cost-effective LLM deployment. ## Case Studies {#sec-inference-scale-case-studies-9689} @@ -5045,7 +5045,7 @@ if gpt4_queue_depth > threshold: **Lessons learned**: 1. Continuous batching is essential for LLM serving at scale -2. Prefix caching provides 2-3$\times$ efficiency for conversational workloads +2. Prefix caching provides 2-3 $\times$ efficiency for conversational workloads 3. Multi-tier rate limiting prevents cascade failures from traffic spikes ### Google Search Ranking {#sec-inference-scale-google-search-ranking-aab2} @@ -5105,7 +5105,7 @@ To meet this latency budget while evaluating thousands of documents, the system | **L2 (Second pass)** | Small transformer | 1,000 → 100 | 50ms | | **L3 (Final rank)** | Large ensemble | 100 → 10 | 100ms | -This achieves 100$\times$ cost reduction compared to running L3 on all candidates. +This achieves 100 $\times$ cost reduction compared to running L3 on all candidates. **Speculative execution**: Given tight latency budgets, Google uses speculative execution for model ensembles: @@ -5274,11 +5274,11 @@ While larger GPU memory enables larger batches for models that fit in memory, th **Pitfall: Neglecting multi-tenancy isolation until production.** -In development and staging, single-tenant deployments work well. In production, noisy neighbors cause sudden, unpredictable performance degradation that is difficult to diagnose and resolve. A tenant bursting to 5$\times$ normal traffic can degrade latency for all other tenants on shared infrastructure. As emphasized in @sec-inference-scale-multitenancy-isolation-688c, resource quotas, priority scheduling, and bulkhead isolation must be designed into the system from the start, not retrofitted after production incidents. To distill the most important lessons from this chapter, here are the *three things to remember*. +In development and staging, single-tenant deployments work well. In production, noisy neighbors cause sudden, unpredictable performance degradation that is difficult to diagnose and resolve. A tenant bursting to 5 $\times$ normal traffic can degrade latency for all other tenants on shared infrastructure. As emphasized in @sec-inference-scale-multitenancy-isolation-688c, resource quotas, priority scheduling, and bulkhead isolation must be designed into the system from the start, not retrofitted after production incidents. To distill the most important lessons from this chapter, here are the *three things to remember*. ::: {.callout-important title="Three Things to Remember"} -1. **Serving cost dominates training cost over a model's lifetime.** For high-volume applications, serving cost exceeds training cost by 100$\times$ or more. Every percentage point of serving efficiency improvement yields ongoing cost reduction. Optimize serving ruthlessly. +1. **Serving cost dominates training cost over a model's lifetime.** For high-volume applications, serving cost exceeds training cost by 100 $\times$ or more. Every percentage point of serving efficiency improvement yields ongoing cost reduction. Optimize serving ruthlessly. 2. **Different model types require different batching strategies.** Static batching for vision, continuous batching for LLMs, feature-parallel batching for recommendation systems. There is no universal optimal strategy. Match technique to workload. 3. **Power-of-two-choices provides exponential load balancing improvement.** Maximum queue length improves from $O(\log n / \log \log n)$ to $O(\log \log n)$ with minimal overhead (two probes per request). This simple technique should be standard for any distributed inference deployment. @@ -5294,7 +5294,7 @@ We analyzed why different model architectures require distinct batching strategi ::: {.callout-takeaways} -* **The Inference Wall**: Serving cost often dominates training cost by 100$\times$ or more over a model's lifetime, making inference efficiency the primary driver of ML economics. +* **The Inference Wall**: Serving cost often dominates training cost by 100 $\times$ or more over a model's lifetime, making inference efficiency the primary driver of ML economics. * **Prefill vs. Decode**: LLM serving is split into two regimes: compute-bound prefill (input processing) and memory-bandwidth-bound decode (token generation). Optimizing for the "Decode" phase requires high-bandwidth memory (HBM3) or specialized storage-as-memory (HBF). * **Batching is Not Universal**: There is no "optimal" batch size for all models. Vision models scale with batch size; recommendation models scale with feature-parallel shards; LLMs scale via continuous, iteration-level management. * **Power of Two Choices**: Simple, randomized load balancing achieves exponentially better queue balance ($O(\log \log n)$) with minimal overhead, a must-have for any large-scale deployment. diff --git a/book/quarto/contents/vol2/infrastructure/infrastructure.qmd b/book/quarto/contents/vol2/infrastructure/infrastructure.qmd index 6a8dea318d..899e3c4a65 100644 --- a/book/quarto/contents/vol2/infrastructure/infrastructure.qmd +++ b/book/quarto/contents/vol2/infrastructure/infrastructure.qmd @@ -415,7 +415,7 @@ This integrated design simplifies deployment but limits flexibility. Each DGX H1 | **NVLink 4.0** | 900 GB/s | ~0.5 microsecond | Large model training | | **(bidirectional)** | | | | -For models requiring tensor parallelism across GPUs, NVLink's 14$\times$ bandwidth advantage directly translates to training throughput. @sec-distributed-training-systems examines how tensor parallelism splits individual layers across GPUs, creating the tight synchronization requirements that make NVLink essential. PCIe-based systems suffice for data-parallel workloads where gradient synchronization occurs less frequently. +For models requiring tensor parallelism across GPUs, NVLink's 14 $\times$ bandwidth advantage directly translates to training throughput. @sec-distributed-training-systems examines how tensor parallelism splits individual layers across GPUs, creating the tight synchronization requirements that make NVLink essential. PCIe-based systems suffice for data-parallel workloads where gradient synchronization occurs less frequently. #### CPU Infrastructure Roles {#sec-compute-cpu-infrastructure-roles-e47f} @@ -461,11 +461,11 @@ Deep learning training is resilient to low precision but sensitive to dynamic ra **Bfloat16 (Brain Floating Point)**, developed by Google, reallocates bits to use 8 bits for the exponent (matching FP32) and 7 bits for the mantissa. This matches the dynamic range of FP32, meaning numbers that fit in FP32 also fit in BF16. This eliminates the need for loss scaling and stabilizes training for large transformers. The trade-off is lower precision from fewer mantissa bits, but neural networks typically do not need high precision for individual weights, only for aggregate accumulation (which is usually done in FP32). Virtually all modern large language models, including GPT-4, PaLM, and Llama, are trained using BF16. ::: -The H100 delivers approximately 3$\times$ the tensor TFLOPS of A100 at 1.75$\times$ the power. To derive the efficiency improvement, A100 achieves `{python} a100_tflops_fp16` TF / `{python} a100_tdp` W = `{python} a100_tflops_per_watt` TF/W, while H100 achieves `{python} h100_tflops_fp16` TF / `{python} h100_tdp` W = `{python} h100_tflops_per_watt` TF/W, yielding approximately 80% improvement in FLOPS/watt. Memory bandwidth increases proportionally, maintaining the compute-to-memory ratio critical for transformer models. +The H100 delivers approximately 3 $\times$ the tensor TFLOPS of A100 at 1.75 $\times$ the power. To derive the efficiency improvement, A100 achieves `{python} a100_tflops_fp16` TF / `{python} a100_tdp` W = `{python} a100_tflops_per_watt` TF/W, while H100 achieves `{python} h100_tflops_fp16` TF / `{python} h100_tdp` W = `{python} h100_tflops_per_watt` TF/W, yielding approximately 80% improvement in FLOPS/watt. Memory bandwidth increases proportionally, maintaining the compute-to-memory ratio critical for transformer models. **Tensor Core Utilization.** Tensor Cores[^fn-tensor-cores] accelerate matrix operations but require specific data layouts and sizes for full utilization. Dimensions should be multiples of 8 for FP16 or 16 for INT8 for optimal performance. Underutilized Tensor Cores represent the most common source of poor GPU efficiency in production, with many workloads achieving only 30 to 50 percent of theoretical peak FLOPS. -[^fn-tensor-cores]: **Tensor Cores**: Specialized matrix-multiply-accumulate units introduced in NVIDIA Volta (2017) that perform 4$\times$4 matrix operations in a single clock cycle. Unlike general-purpose CUDA cores, Tensor Cores are optimized for the fused multiply-add pattern $D = A \times B + C$ that dominates neural network computation. H100 Tensor Cores support FP8, FP16, BF16, TF32, and INT8 formats. +[^fn-tensor-cores]: **Tensor Cores**: Specialized matrix-multiply-accumulate units introduced in NVIDIA Volta (2017) that perform 4 $\times$ 4 matrix operations in a single clock cycle. Unlike general-purpose CUDA cores, Tensor Cores are optimized for the fused multiply-add pattern $D = A \times B + C$ that dominates neural network computation. H100 Tensor Cores support FP8, FP16, BF16, TF32, and INT8 formats. **NVLink Topology.** Within a node, NVSwitch provides full-bandwidth connectivity between all GPUs. Across nodes, NVLink Network, available in H100 and later, extends high-bandwidth connectivity, though at reduced bandwidth compared to intra-node links. @sec-communication-collective-operations examines how topology-aware job placement exploits these bandwidth asymmetries to minimize communication overhead in multi-node training. @@ -505,7 +505,7 @@ The ML accelerator landscape continues to diversify as organizations optimize fo **Inference-Optimized Accelerators.** Training and inference present different requirements. Training needs high-precision arithmetic, large memory for activations and optimizer state, and high interconnect bandwidth. Inference prioritizes low latency, high throughput, and power efficiency. Recent analysis by Ma and Patterson [@ma2024challenges] further refines this distinction by separating the *prefill* phase (compute-bound processing of input tokens) from the *decode* phase (memory-bandwidth-bound generation of output tokens). The decode phase, being autoregressive, requires reading the entire model weight set for every token generated, making memory bandwidth, not FLOPS, the primary determinant of performance. This "memory wall" drives the design of inference-specialized chips that sacrifice raw compute density for massive memory bandwidth and capacity, optimizing for the unique token-by-token access pattern of large language models. -Accelerators like Google's TPU Inference chips and AWS Inferentia optimize for these characteristics, achieving 2–4$\times$ better performance per watt than training-focused hardware for appropriate workloads. +Accelerators like Google's TPU Inference chips and AWS Inferentia optimize for these characteristics, achieving 2–4 $\times$ better performance per watt than training-focused hardware for appropriate workloads. **Emerging Architectures.** Several companies offer alternative approaches. Cerebras WSE uses wafer-scale integration[^fn-wafer-scale] to place an entire ML accelerator on a single silicon wafer, eliminating chip-to-chip communication for models that fit on-chip. Graphcore IPU employs a Bulk Synchronous Parallel[^fn-bsp] execution model with distributed on-chip memory targeting sparse and dynamic workloads. SambaNova provides reconfigurable dataflow architecture for enterprise AI applications. @@ -693,7 +693,7 @@ For a 256-GPU deployment with 2:1 oversubscription: $$C_{\text{network}} \approx 32 \times \$15,000 + 512 \times \$800 + 256 \times \$3,000 \approx \$1,660,000$$ -Refresh cycle planning significantly impacts TCO calculations. GPU generations advance every 2 to 3 years with typical performance improvements of 2–3$\times$ per generation. Organizations must balance the benefits of newer hardware against the disruption costs of migration. A common strategy employs staggered refresh cycles, replacing 25 to 33% of infrastructure annually to maintain competitive capability while avoiding wholesale replacement costs. +Refresh cycle planning significantly impacts TCO calculations. GPU generations advance every 2 to 3 years with typical performance improvements of 2–3 $\times$ per generation. Organizations must balance the benefits of newer hardware against the disruption costs of migration. A common strategy employs staggered refresh cycles, replacing 25 to 33% of infrastructure annually to maintain competitive capability while avoiding wholesale replacement costs. #### Facility Costs {#sec-compute-facility-costs-f185} @@ -872,7 +872,7 @@ where $r$ is the discount rate reflecting cost of capital. For a 256-GPU cluster ``` ::: -The NPV at 8% discount rate equals approximately $24.1 million, yielding a 4-year cost per GPU-hour of $4.30 at 70% utilization. This compares favorably to cloud A100 pricing of $3-4/hour only when accounting for the H100's 3$\times$ performance advantage, yielding effective cost per computation approximately 40% below cloud alternatives at this utilization level. +The NPV at 8% discount rate equals approximately $24.1 million, yielding a 4-year cost per GPU-hour of $4.30 at 70% utilization. This compares favorably to cloud A100 pricing of $3-4/hour only when accounting for the H100's 3 $\times$ performance advantage, yielding effective cost per computation approximately 40% below cloud alternatives at this utilization level. Power cost sensitivity analysis reveals the importance of electricity pricing in deployment decisions. A $0.04/kWh difference in electricity rates shifts the 4-year TCO by approximately $2.7 million for a 256-GPU cluster, potentially changing the optimal deployment strategy. Organizations with access to low-cost renewable energy enjoy structural cost advantages that compound over multi-year infrastructure investments. @@ -970,7 +970,7 @@ Tesla's Dojo system represents the custom silicon approach to ML infrastructure: The Dojo D1 chip provides 1024 custom-designed cores in a 645 mm^2^ die. Each core combines an 8-wide vector unit, 64-bit scalar unit, and 1.25 MB of SRAM, yielding approximately 22.6 TFLOPS of BF16[^fn-bf16] compute per chip. The design optimizes for convolutional and attention operations typical of vision models, with dataflow execution patterns that minimize memory traffic. -[^fn-bf16]: **BF16 (Brain Floating Point)**: A 16-bit floating point format with the same 8-bit exponent as FP32 but only 7 mantissa bits (versus 23 in FP32). Developed by Google for TPUs, BF16 matches FP32's dynamic range, avoiding the overflow/underflow issues of FP16 that require loss scaling. BF16 has become the default training precision for transformers, offering 2$\times$ memory savings with minimal accuracy impact. +[^fn-bf16]: **BF16 (Brain Floating Point)**: A 16-bit floating point format with the same 8-bit exponent as FP32 but only 7 mantissa bits (versus 23 in FP32). Developed by Google for TPUs, BF16 matches FP32's dynamic range, avoiding the overflow/underflow issues of FP16 that require loss scaling. BF16 has become the default training precision for transformers, offering 2 $\times$ memory savings with minimal accuracy impact. Twenty-five D1 chips mount on a single training tile, connected via a 2D mesh interconnect providing 4 TB/s aggregate bandwidth. Six tiles combine into a system tray, and multiple trays assemble into a complete ExaPOD delivering over 1 exaFLOP of aggregate compute. The modular architecture enables deployments from single tiles (0.5 PFLOPS) to multi-ExaPOD installations. @@ -1004,11 +1004,11 @@ Engineers assume training time scales linearly with GPU count. In production, co **Fallacy:** ***Peak FLOPS determines training throughput.*** -Procurement teams select accelerators by comparing peak FLOPS: H100 delivers `{python} h100_tflops_fp16` TF of FP16 compute. In production workloads, memory bandwidth limits performance. The roofline model in @sec-compute-warehousescale-computer-384e shows that arithmetic intensity below the ridge point (~290 FLOP/byte for H100) makes accelerators memory-bound. Most LLM training operates at 50 to 100 FLOP/byte, achieving only 168 to 335 TF effective throughput (17 to 34% of peak). Organizations that compare accelerators by peak FLOPS alone make purchasing decisions that cost 1.5 to 2$\times$ more per actual training FLOP delivered. For attention-dominated workloads, lower-FLOPS accelerators with higher memory bandwidth often outperform higher-FLOPS alternatives. +Procurement teams select accelerators by comparing peak FLOPS: H100 delivers `{python} h100_tflops_fp16` TF of FP16 compute. In production workloads, memory bandwidth limits performance. The roofline model in @sec-compute-warehousescale-computer-384e shows that arithmetic intensity below the ridge point (~290 FLOP/byte for H100) makes accelerators memory-bound. Most LLM training operates at 50 to 100 FLOP/byte, achieving only 168 to 335 TF effective throughput (17 to 34% of peak). Organizations that compare accelerators by peak FLOPS alone make purchasing decisions that cost 1.5 to 2 $\times$ more per actual training FLOP delivered. For attention-dominated workloads, lower-FLOPS accelerators with higher memory bandwidth often outperform higher-FLOPS alternatives. **Fallacy:** ***All ML infrastructure should be GPU-based.*** -Teams assume GPUs optimize every ML workload. At scale, hybrid architectures deliver superior economics for embedding-heavy workloads. Recommendation systems, which constitute 80 to 90% of inference volume at Meta and Google, require random access to terabytes of embedding tables that cannot fit in GPU HBM. These memory-bound lookups perform poorly on GPUs optimized for dense compute. Meta's production infrastructure uses CPU clusters for embedding lookups while GPU clusters process dense neural network layers, achieving 3$\times$ better cost efficiency than GPU-only deployments. Organizations that deploy GPU-only infrastructure for diverse workloads waste 40 to 60% of capacity on tasks CPUs handle more efficiently. +Teams assume GPUs optimize every ML workload. At scale, hybrid architectures deliver superior economics for embedding-heavy workloads. Recommendation systems, which constitute 80 to 90% of inference volume at Meta and Google, require random access to terabytes of embedding tables that cannot fit in GPU HBM. These memory-bound lookups perform poorly on GPUs optimized for dense compute. Meta's production infrastructure uses CPU clusters for embedding lookups while GPU clusters process dense neural network layers, achieving 3 $\times$ better cost efficiency than GPU-only deployments. Organizations that deploy GPU-only infrastructure for diverse workloads waste 40 to 60% of capacity on tasks CPUs handle more efficiently. **Pitfall:** ***Ignoring power and cooling constraints during infrastructure planning.*** @@ -1020,7 +1020,7 @@ Operators calculate network bandwidth but ignore latency, topology, and software **Fallacy:** ***Cloud computing costs scale linearly with usage.*** -Organizations assume cloud expenses grow proportionally with compute consumption. In production, data egress fees and storage costs create nonlinear scaling. Training a 70B parameter model generates 200 to 400 GB of checkpoints every few hours; with 5 checkpoint retention, storage reaches 1 to 2 TB per experiment. Cloud storage at $0.023 per GB-month costs $23 to $46 per month per experiment, but egress fees for downloading checkpoints cost $0.09 per GB ($18 to $36 per full checkpoint download). Organizations running 100 concurrent experiments accumulate $2,300 to $4,600 per month in storage costs alone. The break-even point where on-premises infrastructure becomes more economical occurs at 40 to 60% sustained utilization. Teams that migrate to cloud assuming linear scaling discover total costs 1.5 to 2.5$\times$ higher than projected. +Organizations assume cloud expenses grow proportionally with compute consumption. In production, data egress fees and storage costs create nonlinear scaling. Training a 70B parameter model generates 200 to 400 GB of checkpoints every few hours; with 5 checkpoint retention, storage reaches 1 to 2 TB per experiment. Cloud storage at $0.023 per GB-month costs $23 to $46 per month per experiment, but egress fees for downloading checkpoints cost $0.09 per GB ($18 to $36 per full checkpoint download). Organizations running 100 concurrent experiments accumulate $2,300 to $4,600 per month in storage costs alone. The break-even point where on-premises infrastructure becomes more economical occurs at 40 to 60% sustained utilization. Teams that migrate to cloud assuming linear scaling discover total costs 1.5 to 2.5 $\times$ higher than projected. **Pitfall:** ***Underestimating operational complexity of distributed schedulers.*** diff --git a/book/quarto/contents/vol2/introduction/introduction.qmd b/book/quarto/contents/vol2/introduction/introduction.qmd index 5e43218a74..2923c8f8fa 100644 --- a/book/quarto/contents/vol2/introduction/introduction.qmd +++ b/book/quarto/contents/vol2/introduction/introduction.qmd @@ -132,7 +132,7 @@ The history of machine learning is defined by scale. Each major capability leap Compute requirements have grown exponentially. AlexNet (2012) trained on two GTX 580 GPUs for approximately 5-6 days [@krizhevsky2012imagenet]. BERT (2018) required 64 TPU[^fn-tpu] chips for 4 days, roughly 6,144 chip hours [@devlin2018bert]. -[^fn-tpu]: **Tensor Processing Unit (TPU)**: Google's custom-designed ASIC optimized specifically for neural network computation. Unlike general-purpose GPUs, TPUs implement a systolic array architecture that excels at the matrix multiplications dominating deep learning workloads. TPU v4 chips deliver approximately `{python} tpuv4_bf16_tflops` TFLOPS of bfloat16 performance at 175 watts, roughly 2.5$\times$ the performance per watt of contemporary GPUs for transformer training. The latest TPU v5p chips are interconnected via Inter-Core Interconnect (ICI) providing 4,800 Gb/s of bandwidth per chip, enabling tight coupling for distributed training across TPU pods containing thousands of chips. While GPUs offer broader flexibility for varied workloads, TPUs demonstrate how hardware-software co-design enables significant efficiency gains for specific computational patterns. +[^fn-tpu]: **Tensor Processing Unit (TPU)**: Google's custom-designed ASIC optimized specifically for neural network computation. Unlike general-purpose GPUs, TPUs implement a systolic array architecture that excels at the matrix multiplications dominating deep learning workloads. TPU v4 chips deliver approximately `{python} tpuv4_bf16_tflops` TFLOPS of bfloat16 performance at 175 watts, roughly 2.5 $\times$ the performance per watt of contemporary GPUs for transformer training. The latest TPU v5p chips are interconnected via Inter-Core Interconnect (ICI) providing 4,800 Gb/s of bandwidth per chip, enabling tight coupling for distributed training across TPU pods containing thousands of chips. While GPUs offer broader flexibility for varied workloads, TPUs demonstrate how hardware-software co-design enables significant efficiency gains for specific computational patterns. GPT-3 (2020) consumed an estimated `{python} gpt3_training_ops_sci` FLOPS during training, requiring thousands of V100 GPUs running for weeks [@brown2020language]. PaLM (2022) trained on 6,144 TPU v4 chips for roughly 60 days, consuming approximately 10²⁴ FLOPS [@chowdhery2022palm]. GPT-4 (2023) reportedly trained on approximately 25,000 A100 GPUs over 90-100 days [@openai2023gpt4]. This progression represents approximately a 10-million-fold increase in training compute over a single decade, from roughly 10¹⁸ FLOPS for AlexNet to 10²⁵ FLOPS for GPT-4 [@sevilla2022compute; @amodei2018ai]. diff --git a/book/quarto/contents/vol2/ops_scale/ops_scale.qmd b/book/quarto/contents/vol2/ops_scale/ops_scale.qmd index b747521660..795b85e5f5 100644 --- a/book/quarto/contents/vol2/ops_scale/ops_scale.qmd +++ b/book/quarto/contents/vol2/ops_scale/ops_scale.qmd @@ -1020,7 +1020,7 @@ $$t_{5\%} = \frac{10,000}{1,000,000 \times 0.05} = 0.2 \text{ hours} = 12 \text{ The organization might configure: -- 1% for 2 hours (2$\times$ minimum for buffer) +- 1% for 2 hours (2 $\times$ minimum for buffer) - 5% for 30 minutes - 25% for 30 minutes - 50% for 1 hour @@ -1286,9 +1286,9 @@ Shadow deployment is less critical for: Recommendation systems use interleaving experiments[^fn-interleaving] for more efficient comparison than traditional A/B testing [@chapelle2012large]. Rather than splitting users between variants, interleaving presents items from both variants to each user, then measures which items users engage with. -[^fn-interleaving]: **Interleaving Experiments**: First developed for search engine evaluation, interleaving combines results from two rankers into a single list shown to users. Credit is assigned based on which ranker's items receive clicks. This approach requires 10-100$\times$ fewer samples than A/B testing because each user provides direct comparison signals rather than contributing to aggregate statistics that must be compared across populations. +[^fn-interleaving]: **Interleaving Experiments**: First developed for search engine evaluation, interleaving combines results from two rankers into a single list shown to users. Credit is assigned based on which ranker's items receive clicks. This approach requires 10-100 $\times$ fewer samples than A/B testing because each user provides direct comparison signals rather than contributing to aggregate statistics that must be compared across populations. -The key insight is statistical efficiency. An interleaving experiment requires 10$\times$ to 100$\times$ fewer samples to detect the same effect size compared to A/B testing [@kohavi2009controlled], because each user provides direct comparison signals rather than contributing to aggregate statistics. +The key insight is statistical efficiency. An interleaving experiment requires 10 $\times$ to 100 $\times$ fewer samples to detect the same effect size compared to A/B testing [@kohavi2009controlled], because each user provides direct comparison signals rather than contributing to aggregate statistics. Interleaving implementation: @@ -2451,7 +2451,7 @@ $$\text{Cost per inference} = \frac{\text{Total serving cost}}{\text{Total infer This metric enables: -- Comparing model versions (does the accuracy gain justify 2$\times$ inference cost?) +- Comparing model versions (does the accuracy gain justify 2 $\times$ inference cost?) - Evaluating optimization investments (quantization reduced cost per inference by 40%) - Capacity planning (at projected traffic, monthly serving cost will be X) - Business decisions (can we offer this feature profitably at the expected price point?) @@ -2484,15 +2484,15 @@ These controls should inform rather than block. The goal is cost awareness, not **Cost-Quality Tradeoffs** -Model selection should explicitly consider cost alongside accuracy. @tbl-ops-scale-cost-quality illustrates the diminishing returns: moving from small to medium model yields 3% accuracy gain for 10$\times$ training cost increase, while medium to large yields only 1% additional accuracy for another 10$\times$ cost, a pattern that should inform deployment decisions: +Model selection should explicitly consider cost alongside accuracy. @tbl-ops-scale-cost-quality illustrates the diminishing returns: moving from small to medium model yields 3% accuracy gain for 10 $\times$ training cost increase, while medium to large yields only 1% additional accuracy for another 10 $\times$ cost, a pattern that should inform deployment decisions: -| **Model** | **Accuracy** | **Training Cost** | **Serving Cost/1K** | **Value Judgment** | -|:-----------|-------------:|------------------:|--------------------:|:----------------------------------| -| **Small** | 92% | \$500 | \$0.10 | Baseline | -| **Medium** | 95% | \$5,000 | \$0.50 | 3% accuracy for 10$\times$ cost | -| **Large** | 96% | \$50,000 | \$2.00 | Additional 1% for 10$\times$ more | +| **Model** | **Accuracy** | **Training Cost** | **Serving Cost/1K** | **Value Judgment** | +|:-----------|-------------:|------------------:|--------------------:|:-----------------------------------| +| **Small** | 92% | \$500 | \$0.10 | Baseline | +| **Medium** | 95% | \$5,000 | \$0.50 | 3% accuracy for 10 $\times$ cost | +| **Large** | 96% | \$50,000 | \$2.00 | Additional 1% for 10 $\times$ more | -: **Cost-Quality Tradeoff Analysis**: Diminishing returns in model scaling. Small-to-medium model transition yields 3% accuracy gain for 10$\times$ cost increase; medium-to-large yields only 1% additional accuracy for another 10$\times$ cost. This pattern demonstrates why explicit cost-quality analysis should inform model selection rather than defaulting to larger architectures. {#tbl-ops-scale-cost-quality} +: **Cost-Quality Tradeoff Analysis**: Diminishing returns in model scaling. Small-to-medium model transition yields 3% accuracy gain for 10 $\times$ cost increase; medium-to-large yields only 1% additional accuracy for another 10 $\times$ cost. This pattern demonstrates why explicit cost-quality analysis should inform model selection rather than defaulting to larger architectures. {#tbl-ops-scale-cost-quality} For many applications, the marginal accuracy gain does not justify the cost increase. Making these tradeoffs explicit prevents defaulting to the largest available model. @@ -2645,7 +2645,7 @@ where: - $C_{engineer}$ is the engineering cost per experiment (time allocation) - $C_{compute}$ is the compute cost per experiment -A critical but often overlooked factor: failed experiments have real cost. If 90% of experiments do not improve production metrics, the effective cost per successful experiment is 10$\times$ the nominal experiment cost. This motivates investment in experiment infrastructure that reduces $T_{exp}$ and improves experiment success rates. +A critical but often overlooked factor: failed experiments have real cost. If 90% of experiments do not improve production metrics, the effective cost per successful experiment is 10 $\times$ the nominal experiment cost. This motivates investment in experiment infrastructure that reduces $T_{exp}$ and improves experiment success rates. **Worked Example: Startup vs. Production Company TCO** @@ -2663,30 +2663,30 @@ A critical but often overlooked factor: failed experiments have real cost. If 90 - Dedicated ML platform team (15 engineers) - Hybrid cloud/on-premise infrastructure -| **Cost Component** | **Startup** | **Production Company** | **Scaling Factor** | -|:-------------------|-------------------:|-----------------------:|---------------------------------------:| -| **Training** | \$5,000/month | \$150,000/month | 30x (more models, larger) | -| **Inference** | \$2,000/month | \$400,000/month | 200x (100$\times$ users, optimization) | -| **Data** | \$500/month | \$80,000/month | 160x (superlinear with users) | -| **Iteration** | \$40,000/month | \$350,000/month | 8.75x (team size, experiments) | -| **Total TCO** | **\$47,500/month** | **\$980,000/month** | **20.6x** | -| **Dominant Cost** | Iteration (84%) | Inference (41%) | | +| **Cost Component** | **Startup** | **Production Company** | **Scaling Factor** | +|:-------------------|-------------------:|-----------------------:|----------------------------------------:| +| **Training** | \$5,000/month | \$150,000/month | 30x (more models, larger) | +| **Inference** | \$2,000/month | \$400,000/month | 200x (100 $\times$ users, optimization) | +| **Data** | \$500/month | \$80,000/month | 160x (superlinear with users) | +| **Iteration** | \$40,000/month | \$350,000/month | 8.75x (team size, experiments) | +| **Total TCO** | **\$47,500/month** | **\$980,000/month** | **20.6x** | +| **Dominant Cost** | Iteration (84%) | Inference (41%) | | -: **TCO Comparison: Startup vs. Production Company**. Cost structure shifts dramatically with scale. Startups are dominated by iteration costs (engineering salaries for experimentation), while production companies see inference costs dominate as serving volume grows. The 100$\times$ user increase yields only 20$\times$ TCO increase due to optimization effects, but note the superlinear 160$\times$ scaling in data costs. {#tbl-tco-comparison} +: **TCO Comparison: Startup vs. Production Company**. Cost structure shifts dramatically with scale. Startups are dominated by iteration costs (engineering salaries for experimentation), while production companies see inference costs dominate as serving volume grows. The 100 $\times$ user increase yields only 20 $\times$ TCO increase due to optimization effects, but note the superlinear 160 $\times$ scaling in data costs. {#tbl-tco-comparison} **Key Insights from Comparison:** 1. **Cost structure inversion**: Startups spend 84% on iteration (people), production companies spend 41% on inference (infrastructure). This shift demands different optimization strategies. -2. **Sublinear TCO scaling**: 100$\times$ users yields 20$\times$ TCO due to economies of scale in inference (batching, utilization) and amortized training costs across larger user base. +2. **Sublinear TCO scaling**: 100 $\times$ users yields 20 $\times$ TCO due to economies of scale in inference (batching, utilization) and amortized training costs across larger user base. -3. **Data cost superlinearity**: Data costs scale 160$\times$ for 100$\times$ users because storage requirements grow with user history, and processing costs increase with feature complexity. +3. **Data cost superlinearity**: Data costs scale 160 $\times$ for 100 $\times$ users because storage requirements grow with user history, and processing costs increase with feature complexity. -4. **Iteration efficiency**: Production company runs 10$\times$ more experiments but iteration cost only grows 8.75$\times$ due to platform automation reducing per-experiment overhead. +4. **Iteration efficiency**: Production company runs 10 $\times$ more experiments but iteration cost only grows 8.75 $\times$ due to platform automation reducing per-experiment overhead. **TCO Sensitivity Analysis** -Understanding how TCO responds to key parameters enables strategic planning. @tbl-tco-sensitivity shows the impact of 2$\times$ increases in key parameters on each cost component: +Understanding how TCO responds to key parameters enables strategic planning. @tbl-tco-sensitivity shows the impact of 2 $\times$ increases in key parameters on each cost component: | **Parameter** | **Change** | **Training** | **Inference** | **Data** | **Total Impact** | |:---------------------|:----------:|:------------:|:-------------:|:--------:|:----------------:| @@ -2696,12 +2696,12 @@ Understanding how TCO responds to key parameters enables strategic planning. @tb | **Model size** | 2x | +150% | +120% | +30% | +55% | | **Retraining freq.** | 2x | +100% | -- | +40% | +25% | -: **TCO Sensitivity Analysis**. Impact of 2$\times$ increase in key parameters on cost components. Model count has the highest total impact (85%) because it affects all components, while retraining frequency has the lowest (25%) as it affects only training and associated data costs. User growth shows superlinear data cost impact (120%) due to storage and processing requirements scaling faster than user count. {#tbl-tco-sensitivity} +: **TCO Sensitivity Analysis**. Impact of 2 $\times$ increase in key parameters on cost components. Model count has the highest total impact (85%) because it affects all components, while retraining frequency has the lowest (25%) as it affects only training and associated data costs. User growth shows superlinear data cost impact (120%) due to storage and processing requirements scaling faster than user count. {#tbl-tco-sensitivity} **Nonlinear Effects:** -- **User scaling**: Data costs grow superlinearly (120% for 2$\times$ users) due to user history accumulation and cross-user feature computation. -- **Model size scaling**: Training cost grows superlinearly (150% for 2$\times$ parameters) due to increased memory requirements forcing multi-GPU configurations. +- **User scaling**: Data costs grow superlinearly (120% for 2 $\times$ users) due to user history accumulation and cross-user feature computation. +- **Model size scaling**: Training cost grows superlinearly (150% for 2 $\times$ parameters) due to increased memory requirements forcing multi-GPU configurations. - **Model count**: Creates multiplicative effects across all components, making portfolio growth the most expensive scaling dimension. **Decision Framework: Speed vs. Efficiency** @@ -2774,7 +2774,7 @@ TCO analysis should inform architectural choices, not just operational optimizat 1. **Build vs. buy**: Platform services with usage-based pricing may have lower TCO than self-managed infrastructure despite higher unit costs, especially when iteration costs dominate. -2. **Model architecture selection**: A model requiring 2$\times$ training cost but 0.5$\times$ inference cost may have lower TCO at scale where inference dominates. +2. **Model architecture selection**: A model requiring 2 $\times$ training cost but 0.5 $\times$ inference cost may have lower TCO at scale where inference dominates. 3. **Retraining frequency**: More frequent retraining increases $C_{train}$ and $C_{data}$ but may reduce $C_{iter}$ by catching drift earlier and avoiding emergency interventions. @@ -2996,7 +2996,7 @@ Major recommendation systems process billions of feature requests daily: - 1 billion daily recommendations - 100 features per recommendation - 100 billion feature lookups per day -- 1.1 million lookups per second average, 5-10$\times$ peaks +- 1.1 million lookups per second average, 5-10 $\times$ peaks **Latency Requirements** @@ -3736,7 +3736,7 @@ Engineers configure alerts on every metric for every model to ensure comprehensi **Fallacy:** ***Platform investment makes sense only after reaching 100+ models.*** -Teams defer platform engineering, assuming the overhead does not justify until "massive scale." In reality, @eq-platform-roi shows platform ROI becomes positive at 20-50 models, not 100+. Technical debt from fragmented per-model practices compounds over time: configuration debt grows when each of 40 models maintains 847-line YAML files with no validation schema, causing 35% of deployment delays and costing 960 engineer-hours annually. Pipeline glue code debt accumulates when 23 different preprocessing scripts with 62% duplication require 12 engineer-hours weekly debugging pipeline breaks. By the time organizations reach 100 models, the migration cost from fragmented infrastructure to unified platform exceeds the cost of building the platform initially by 3-5$\times$. The economic threshold is clear: with 50 models requiring 40 hours monthly operational work each, platform investment of \$2M saves \$2M annually, paying for itself within 12 months. +Teams defer platform engineering, assuming the overhead does not justify until "massive scale." In reality, @eq-platform-roi shows platform ROI becomes positive at 20-50 models, not 100+. Technical debt from fragmented per-model practices compounds over time: configuration debt grows when each of 40 models maintains 847-line YAML files with no validation schema, causing 35% of deployment delays and costing 960 engineer-hours annually. Pipeline glue code debt accumulates when 23 different preprocessing scripts with 62% duplication require 12 engineer-hours weekly debugging pipeline breaks. By the time organizations reach 100 models, the migration cost from fragmented infrastructure to unified platform exceeds the cost of building the platform initially by 3-5 $\times$. The economic threshold is clear: with 50 models requiring 40 hours monthly operational work each, platform investment of \$2M saves \$2M annually, paying for itself within 12 months. **Pitfall:** ***Treating all deployments with uniform rollout procedures regardless of risk profile.*** diff --git a/book/quarto/contents/vol2/optimization/_shelved_automl_section.qmd b/book/quarto/contents/vol2/optimization/_shelved_automl_section.qmd index 5762219bdc..a140740e24 100644 --- a/book/quarto/contents/vol2/optimization/_shelved_automl_section.qmd +++ b/book/quarto/contents/vol2/optimization/_shelved_automl_section.qmd @@ -66,7 +66,7 @@ AutoML systems address multiple optimization dimensions simultaneously: - **Compression selection**: Automated selection of pruning thresholds, sparsity patterns, and quantization levels based on deployment constraints [@Wu2016] - **Hardware-aware optimization**: Models tailored to specific devices by adjusting computational workloads and memory access patterns [@Cai2020] -[^fn-bayesian-optimization]: **Bayesian Optimization**: Achieves 10-50$\times$ sample efficiency vs. random search using Gaussian processes to model objective uncertainty. Optuna and Ray Tune provide production-ready implementations. +[^fn-bayesian-optimization]: **Bayesian Optimization**: Achieves 10-50 $\times$ sample efficiency vs. random search using Gaussian processes to model objective uncertainty. Optuna and Ray Tune provide production-ready implementations. Modern platforms (Google AutoML, Amazon SageMaker Autopilot, Microsoft Azure AutoML) integrate these capabilities into end-to-end pipelines [@Li2021]. diff --git a/book/quarto/contents/vol2/parts/inference_principles.qmd b/book/quarto/contents/vol2/parts/inference_principles.qmd index 7ae26f1955..4e86fc07b5 100644 --- a/book/quarto/contents/vol2/parts/inference_principles.qmd +++ b/book/quarto/contents/vol2/parts/inference_principles.qmd @@ -10,7 +10,7 @@ Throughput scales with batch size (sharing weight loads across multiple requests ::: ::: {#nte-serving-cost-dominance .callout-principle icon=false title="The Serving Cost Dominance Law"} -**The Principle**: Over a successful model's lifetime, Inference OpEx (Operational Expenditure) exceeds Training CapEx (Capital Expenditure) by 100–1000$\times$. +**The Principle**: Over a successful model's lifetime, Inference OpEx (Operational Expenditure) exceeds Training CapEx (Capital Expenditure) by 100–1000 $\times$. **The Engineering Implication**: Inference efficiency is the primary economic driver of ML systems. Optimization efforts (Quantization, Distillation) should be disproportionately focused on the inference path, even if they increase training cost. diff --git a/book/quarto/contents/vol2/parts/production_principles.qmd b/book/quarto/contents/vol2/parts/production_principles.qmd index e1f0cbcceb..83556d38ec 100644 --- a/book/quarto/contents/vol2/parts/production_principles.qmd +++ b/book/quarto/contents/vol2/parts/production_principles.qmd @@ -11,7 +11,7 @@ Privacy is a budget, not a switch. You cannot "anonymize" data once it is memori ::: ::: {#nte-robustness-compute-penalty .callout-principle icon=false title="The Robustness Compute Penalty"} -**The Principle**: Achieving intrinsic adversarial robustness requires training on perturbations, which demands ~8–10$\times$ more compute per epoch than standard optimization. +**The Principle**: Achieving intrinsic adversarial robustness requires training on perturbations, which demands ~8–10 $\times$ more compute per epoch than standard optimization. **The Engineering Implication**: There is no "free" robustness. Building secure models is computationally expensive. For many applications, it is more efficient to rely on **external guardrails** (input filtering, output verification) than to train intrinsic robustness into the model weights. @@ -22,5 +22,5 @@ There is no "free" robustness. Building secure models is computationally expensi $$ \text{Efficiency} \uparrow \implies \text{Cost} \downarrow \implies \text{Demand} \uparrow \uparrow $$ **The Engineering Implication**: -Making models 10$\times$ more efficient will likely lead to 100$\times$ more usage, not 10$\times$ energy savings (Induced Demand). Sustainability strategies must focus on **absolute limits** (carbon budgets, renewable sourcing) rather than just rate efficiency (FLOPS/watt). +Making models 10 $\times$ more efficient will likely lead to 100 $\times$ more usage, not 10 $\times$ energy savings (Induced Demand). Sustainability strategies must focus on **absolute limits** (carbon budgets, renewable sourcing) rather than just rate efficiency (FLOPS/watt). ::: diff --git a/book/quarto/contents/vol2/parts/scale_principles.qmd b/book/quarto/contents/vol2/parts/scale_principles.qmd index 37bcd91c7d..80326fb40d 100644 --- a/book/quarto/contents/vol2/parts/scale_principles.qmd +++ b/book/quarto/contents/vol2/parts/scale_principles.qmd @@ -7,7 +7,7 @@ If Volume 1 was about optimizing the single machine, Volume 2 is about coordinat $$ L(C) \propto C^{-\alpha} $$ **The Engineering Implication**: -Scale is not an option; it is a requirement for capability. To achieve a 10$\times$ improvement in performance, you typically need a 100x-1000$\times$ increase in compute. This exponential hunger drives the transition from single-server training to warehouse-scale clusters. +Scale is not an option; it is a requirement for capability. To achieve a 10 $\times$ improvement in performance, you typically need a 100x-1000 $\times$ increase in compute. This exponential hunger drives the transition from single-server training to warehouse-scale clusters. ::: ::: {#nte-distributed-step-time .callout-principle icon=false title="The Distributed Step Time Law (Iron Law of Scale)"} diff --git a/book/quarto/contents/vol2/responsible_ai/responsible_ai.qmd b/book/quarto/contents/vol2/responsible_ai/responsible_ai.qmd index 18776da198..8b221c833d 100644 --- a/book/quarto/contents/vol2/responsible_ai/responsible_ai.qmd +++ b/book/quarto/contents/vol2/responsible_ai/responsible_ai.qmd @@ -154,7 +154,7 @@ These principles work in concert to define what it means for a machine learning Implementing responsible AI principles requires computational resources that vary significantly across techniques and deployment contexts. These resource requirements create multifaceted equity considerations that extend beyond individual organizations to encompass broader social and environmental justice concerns. Organizations with limited computing budgets may be unable to implement comprehensive responsible AI protections, potentially creating disparate access to ethical safeguards. State-of-the-art AI systems increasingly require specialized hardware and high-bandwidth connectivity that systematically exclude rural communities, developing regions, and resource-constrained users from accessing advanced AI capabilities. -Environmental justice concerns compound these access barriers through the engineering reality that responsible AI techniques impose significant energy costs. Training differential privacy models requires 15-30% additional compute cycles; real time fairness monitoring adds 10-20 ms latency and continuous CPU overhead; SHAP explanations demand 50–1000$\times$ normal inference compute. These computational requirements translate directly into infrastructure demands: a high traffic system serving responsible AI features to 10 million users requires substantial additional datacenter capacity compared to unconstrained models. +Environmental justice concerns compound these access barriers through the engineering reality that responsible AI techniques impose significant energy costs. Training differential privacy models requires 15-30% additional compute cycles; real time fairness monitoring adds 10-20 ms latency and continuous CPU overhead; SHAP explanations demand 50–1000 $\times$ normal inference compute. These computational requirements translate directly into infrastructure demands: a high traffic system serving responsible AI features to 10 million users requires substantial additional datacenter capacity compared to unconstrained models. The geographic distribution of this computational infrastructure creates systematic inequities that engineers must consider in system design. Data centers supporting AI workloads concentrate in regions with low electricity costs and favorable regulations, areas that often correlate with lower-income communities that experience increased pollution, heat generation, and electrical grid strain while frequently lacking the high-bandwidth connectivity needed to access the AI services these facilities enable. This creates a feedback loop where computational equity depends not only on algorithmic design but on infrastructure placement decisions that affect both system performance and community welfare. The detailed performance characteristics of specific techniques are examined in @sec-responsible-ai-technical-foundations-3436. @@ -550,11 +550,11 @@ The quantitative framework developed here transforms fairness from an abstract p Fairness considerations extend beyond algorithmic outcomes to encompass the computational resources and infrastructure required to deploy responsible AI systems. These broader equity implications, including environmental justice concerns, arise when energy-intensive AI infrastructure is concentrated in already disadvantaged communities[^fn-datacenter-environmental-justice]. -[^fn-datacenter-environmental-justice]: **Datacenter Environmental Justice**: Research suggests that a significant percentage of major cloud computing facilities in the U.S. are located within 10 miles of low-income communities or communities of color. These areas experience increased air pollution from backup generators, higher local temperatures from cooling systems, and strained local electrical grids. Meanwhile, high-speed internet access required for advanced AI services remains limited in many of these same communities, creating a computational equity gap where communities bear environmental costs without receiving proportional benefits. +[^fn-datacenter-environmental-justice]: **Datacenter Environmental Justice**: Research suggests that a significant percentage of major cloud computing facilities in the U.S. are located within 16 km of low-income communities or communities of color. These areas experience increased air pollution from backup generators, higher local temperatures from cooling systems, and strained local electrical grids. Meanwhile, high-speed internet access required for advanced AI services remains limited in many of these same communities, creating a computational equity gap where communities bear environmental costs without receiving proportional benefits. The computational intensity of responsible AI techniques creates a form of digital divide where access to fair, transparent, and accountable AI systems becomes contingent on economic resources. Implementing fairness constraints, differential privacy mechanisms, and comprehensive explainability tools typically increases computational costs by 15-40% compared to unconstrained models. This creates a troubling dynamic where only organizations with substantial computational budgets can afford to deploy genuinely responsible AI systems, while resource-constrained deployments may sacrifice ethical safeguards for efficiency. The result is a two-tiered system where responsible AI becomes a privilege available primarily to well-resourced users and applications, potentially exacerbating existing inequalities rather than addressing them. These resource constraints create democratization challenges, while the broader implications create digital divide and access barriers affecting underserved communities. -These considerations point to a fundamental conclusion: fairness is a system-wide property that arises from the interaction of data engineering practices, modeling choices, evaluation procedures, and decision policies. It cannot be isolated to a single model component or resolved through post hoc adjustments alone. Responsible machine learning design requires treating fairness as a foundational constraint, one that informs architectural choices, workflows, and governance mechanisms throughout the entire lifecycle of the system. This system-wide view extends to all responsible AI principles, which translate into concrete engineering requirements across the ML lifecycle: fairness demands group-level performance metrics and different decision thresholds across populations; explainability requires runtime compute budgets with costs varying from 10-50 ms for gradient methods to 50–1000$\times$ overhead for SHAP analysis; privacy encompasses data governance, consent mechanisms, and lifecycle-aware retention policies; and accountability requires traceability infrastructure including model registries, audit logs, and human override mechanisms. +These considerations point to a fundamental conclusion: fairness is a system-wide property that arises from the interaction of data engineering practices, modeling choices, evaluation procedures, and decision policies. It cannot be isolated to a single model component or resolved through post hoc adjustments alone. Responsible machine learning design requires treating fairness as a foundational constraint, one that informs architectural choices, workflows, and governance mechanisms throughout the entire lifecycle of the system. This system-wide view extends to all responsible AI principles, which translate into concrete engineering requirements across the ML lifecycle: fairness demands group-level performance metrics and different decision thresholds across populations; explainability requires runtime compute budgets with costs varying from 10-50 ms for gradient methods to 50–1000 $\times$ overhead for SHAP analysis; privacy encompasses data governance, consent mechanisms, and lifecycle-aware retention policies; and accountability requires traceability infrastructure including model registries, audit logs, and human override mechanisms. These principles interact and create tensions throughout system development. Privacy-preserving techniques may reduce explainability; fairness constraints may conflict with personalization; robust monitoring increases computational costs. As @tbl-principles-lifecycle demonstrates, each principle manifests across data collection, training, evaluation, deployment, and monitoring phases, reinforcing that responsible AI is not a post-deployment consideration but an architectural commitment. However, the feasibility of implementing these principles depends critically on deployment context: cloud, edge, mobile, and TinyML environments each impose different constraints that shape which responsible AI features are practically achievable. The following summary captures our progress through the lifecycle integration framework. @@ -983,7 +983,7 @@ Post-processing methods, including the application of group-specific thresholds Scalable fairness enforcement often requires more advanced strategies, such as multicalibration[^fn-multicalibration], which ensures that model predictions remain calibrated across a wide range of intersecting subgroups [@hebert2018multicalibration]. -[^fn-multicalibration]: **Multicalibration**: An advanced fairness technique ensuring that model predictions remain well-calibrated across intersecting demographic subgroups simultaneously. Developed by Úrsula Hébert-Johnson and others in 2018, it addresses the limitation that standard calibration can hold globally while failing for specific subgroups. The technique requires 10-100$\times$ more computational resources than simple threshold tuning but can handle thousands of overlapping groups, making it essential for large-scale platforms serving diverse populations. +[^fn-multicalibration]: **Multicalibration**: An advanced fairness technique ensuring that model predictions remain well-calibrated across intersecting demographic subgroups simultaneously. Developed by Úrsula Hébert-Johnson and others in 2018, it addresses the limitation that standard calibration can hold globally while failing for specific subgroups. The technique requires 10-100 $\times$ more computational resources than simple threshold tuning but can handle thousands of overlapping groups, making it essential for large-scale platforms serving diverse populations. Implementing multicalibration at scale requires infrastructure for dynamically generating subgroup partitions, computing per-group calibration error, and integrating fairness audits into automated monitoring systems. These capabilities are typically only available in large-scale, cloud-based deployments with mature observability and metrics pipelines. In constrained environments such as embedded or TinyML systems, where telemetry is limited and model logic is fixed, such techniques are not feasible and fairness must be validated entirely at design time. @@ -1621,7 +1621,7 @@ At its core, adversarial vulnerability stems from an architectural mismatch betw Improving robustness begins at training. Adversarial training, one of the most widely used techniques, augments training data with perturbed examples [@madry2018towards]. Madry and colleagues formulated adversarial training as a min-max optimization problem, training models against adversarial samples generated with Projected Gradient Descent (PGD)[^fn-pgd-attack]. -[^fn-pgd-attack]: **Projected Gradient Descent (PGD)**: An iterative attack algorithm that generates adversarial examples by repeatedly taking gradient steps to maximize loss, then projecting back into an allowed perturbation region (typically an L-infinity ball around the original input). PGD is considered one of the strongest first-order attacks and serves as the standard for adversarial training. The attack typically uses 7-20 iterations with step size 2/255 for 8-bit images. Training against PGD examples adds 3-10$\times$ computational overhead compared to standard training but produces models with substantially improved robustness to unseen attacks. +[^fn-pgd-attack]: **Projected Gradient Descent (PGD)**: An iterative attack algorithm that generates adversarial examples by repeatedly taking gradient steps to maximize loss, then projecting back into an allowed perturbation region (typically an L-infinity ball around the original input). PGD is considered one of the strongest first-order attacks and serves as the standard for adversarial training. The attack typically uses 7-20 iterations with step size 2/255 for 8-bit images. Training against PGD examples adds 3-10 $\times$ computational overhead compared to standard training but produces models with substantially improved robustness to unseen attacks. This approach provides a principled framework for robust optimization that has become foundational in the field. It helps the model learn more stable decision boundaries but typically increases training time and reduces clean-data accuracy. Implementing adversarial training at scale also places demands on data preprocessing pipelines, model checkpointing infrastructure, and validation protocols that can accommodate perturbed inputs. @@ -1637,7 +1637,7 @@ Monitoring infrastructure plays a critical role in maintaining robustness post-d Beyond empirical defenses, formal approaches offer stronger guarantees. Certified defenses[^fn-certified-defenses], such as randomized smoothing [@cohen2019certified], provide probabilistic assurances that a model's output will remain stable within a bounded input region. -[^fn-certified-defenses]: **Certified Defenses**: Robustness guarantees backed by mathematical proofs rather than empirical testing. Unlike adversarial training (which improves robustness but offers no guarantees), certified defenses provide provable bounds on model behavior. Randomized smoothing works by averaging predictions over many noise-perturbed inputs, creating a smoothed classifier with certified robustness radius. The trade-off: certification requires 100-1000$\times$ more inference compute and typically reduces clean accuracy by 5-15%. +[^fn-certified-defenses]: **Certified Defenses**: Robustness guarantees backed by mathematical proofs rather than empirical testing. Unlike adversarial training (which improves robustness but offers no guarantees), certified defenses provide provable bounds on model behavior. Randomized smoothing works by averaging predictions over many noise-perturbed inputs, creating a smoothed classifier with certified robustness radius. The trade-off: certification requires 100-1000 $\times$ more inference compute and typically reduces clean accuracy by 5-15%. Simpler defenses, such as input preprocessing, filter inputs through denoising, compression, or normalization steps to remove adversarial noise. These transformations must be lightweight enough for real-time execution, especially in edge deployments, and robust enough to preserve task-relevant features. Another approach is ensemble modeling, in which predictions are aggregated across multiple diverse models. This increases robustness but adds complexity to inference pipelines, increases memory footprint, and complicates deployment and maintenance workflows. @@ -1661,7 +1661,7 @@ Explainability plays a central role in system validation, error analysis, user t Explainability methods can be broadly categorized based on when they operate and how they relate to model structure. Post-hoc methods are applied after training and treat the model as a black box. These methods do not require access to internal model weights and instead infer influence patterns or feature contributions from model behavior. Common posthoc techniques include feature attribution methods such as input gradients, Integrated Gradients [@sundararajan2017axiomatic][^fn-integrated-gradients], GradCAM[^fn-gradcam] [@selvaraju2017grad], LIME [@ribeiro2016should], and SHAP [@lundberg2017unified]. Sundararajan and colleagues introduced Integrated Gradients by identifying two fundamental axioms---Sensitivity and Implementation Invariance---that attribution methods should satisfy, demonstrating that most prior methods violated these properties. -[^fn-integrated-gradients]: **Integrated Gradients**: An attribution method that computes feature importance by integrating gradients along a path from a baseline input to the actual input. Developed by Mukund Sundararajan and others at Google in 2017, it satisfies mathematical axioms like sensitivity and implementation invariance that simpler gradient methods violate. Computational cost is 50-200$\times$ higher than basic gradients due to path integration, but provides more reliable attributions for deep networks. +[^fn-integrated-gradients]: **Integrated Gradients**: An attribution method that computes feature importance by integrating gradients along a path from a baseline input to the actual input. Developed by Mukund Sundararajan and others at Google in 2017, it satisfies mathematical axioms like sensitivity and implementation invariance that simpler gradient methods violate. Computational cost is 50-200 $\times$ higher than basic gradients due to path integration, but provides more reliable attributions for deep networks. [^fn-gradcam]: **GradCAM (Gradient-weighted Class Activation Mapping)**: A visualization technique that uses gradients to highlight important regions in images for CNN predictions. Created by researchers at Georgia Tech and others in 2017, it generalizes CAM to any CNN architecture without requiring architectural changes. Widely adopted for medical imaging and autonomous vehicles, GradCAM explanations can be computed in 10-50 ms, making them practical for real-time applications. @@ -1673,7 +1673,7 @@ The SHAP framework[^fn-shapley-values] computes each feature's contribution by e Adding debt_ratio (0.55, high) strongly decreases approval by an additional 0.25. Adding credit_score (620, below threshold) moderately decreases approval by 0.12. The final prediction becomes 0.50 - 0.05 - 0.25 - 0.12 = 0.08, corresponding to P(deny) = 0.72. This reveals that the high debt ratio contributed most strongly to the denial (-0.25), followed by the below-average credit score (-0.12), while income had minimal impact (-0.05). Such explanations are actionable: reducing debt ratio below 40% would likely flip the decision. -However, this rigor comes at significant computational cost. This 3-feature example requires evaluating 2³ = 8 feature subsets. For a model with 20 features, SHAP requires 2²⁰ ≈ 1 million subset evaluations, explaining the 50–1000$\times$ computational overhead compared to simple gradient methods. Tree-based SHAP implementations exploit model structure to reduce this to polynomial time, but deep learning models typically require approximation algorithms (KernelSHAP, DeepSHAP) with sampling-based estimation. While SHAP provides theoretically grounded, additive feature attribution that satisfies desirable properties (local accuracy, missingness, consistency), these costs make SHAP impractical for real-time explanation in high-throughput systems without approximation or caching strategies. +However, this rigor comes at significant computational cost. This 3-feature example requires evaluating 2³ = 8 feature subsets. For a model with 20 features, SHAP requires 2²⁰ ≈ 1 million subset evaluations, explaining the 50–1000 $\times$ computational overhead compared to simple gradient methods. Tree-based SHAP implementations exploit model structure to reduce this to polynomial time, but deep learning models typically require approximation algorithms (KernelSHAP, DeepSHAP) with sampling-based estimation. While SHAP provides theoretically grounded, additive feature attribution that satisfies desirable properties (local accuracy, missingness, consistency), these costs make SHAP impractical for real-time explanation in high-throughput systems without approximation or caching strategies. Another posthoc approach involves counterfactual explanations[^fn-counterfactual-explanations], which describe how a model's output would change if the input were modified in specific ways. @@ -1735,7 +1735,7 @@ A more recent research direction is mechanistic interpretability, which seeks to From a systems perspective, explainability introduces a number of architectural dependencies. Explanations must be generated, stored, surfaced, and evaluated within system constraints. The required infrastructure may include explanation APIs, memory for storing attribution maps, visualization libraries, and logging mechanisms that capture intermediate model behavior. Models must often be instrumented with hooks or configured to support repeated evaluations, particularly for explanation methods that require sampling, perturbation, or backpropagation. -These requirements interact directly with deployment constraints and impose quantifiable performance costs that must be factored into system design. SHAP explanations typically require 50–1000$\times$ additional forward passes compared to standard inference, with computational overhead ranging from 200 ms to 5+ seconds per explanation depending on model complexity. LIME similarly requires training surrogate models that add 100-500 ms per explanation. In production deployments, these costs translate to significant infrastructure overhead: a high-traffic system serving 10,000 predictions per second with 10% explanation rate would require 50-500$\times$ additional compute capacity solely for explainability. +These requirements interact directly with deployment constraints and impose quantifiable performance costs that must be factored into system design. SHAP explanations typically require 50–1000 $\times$ additional forward passes compared to standard inference, with computational overhead ranging from 200 ms to 5+ seconds per explanation depending on model complexity. LIME similarly requires training surrogate models that add 100-500 ms per explanation. In production deployments, these costs translate to significant infrastructure overhead: a high-traffic system serving 10,000 predictions per second with 10% explanation rate would require 50-500 $\times$ additional compute capacity solely for explainability. For resource-constrained environments, gradient-based attribution methods offer more efficient alternatives, typically adding only 10-50 ms overhead per explanation by leveraging backpropagation infrastructure already present for training. However, these methods are less reliable for complex models and may produce inconsistent explanations across model updates. Edge deployments often implement explainability through precomputed rule approximations or simplified decision boundaries, sacrificing explanation fidelity for feasible latency profiles under 100 ms. @@ -2283,7 +2283,7 @@ Engineers assume that sufficient data volume and algorithmic sophistication will **Pitfall:** ***Treating explainability as an optional feature rather than a system requirement.*** -Many teams view explainability as a post-deployment addition that can be integrated once core functionality works. This approach fails when explanation requirements fundamentally constrain architecture. As shown in @tbl-responsible-ai-overhead, SHAP explanations increase inference cost by 50 to 200 percent and memory overhead by 20 to 100 percent. A recommendation system serving 100 ms latency requirements at 10,000 QPS cannot retrofit SHAP without violating SLA: SHAP adds 50 to 200 ms per request, increasing total latency to 150-300 ms. The serving infrastructure must be redesigned with explanation budgets from initial architecture, including pre-computed approximations or model selection favoring inherently interpretable architectures. Teams that treat explainability as optional discover during deployment that their deep ensemble achieves required accuracy but cannot meet regulatory explanation requirements without 5$\times$ infrastructure cost increases that exceed project budgets. +Many teams view explainability as a post-deployment addition that can be integrated once core functionality works. This approach fails when explanation requirements fundamentally constrain architecture. As shown in @tbl-responsible-ai-overhead, SHAP explanations increase inference cost by 50 to 200 percent and memory overhead by 20 to 100 percent. A recommendation system serving 100 ms latency requirements at 10,000 QPS cannot retrofit SHAP without violating SLA: SHAP adds 50 to 200 ms per request, increasing total latency to 150-300 ms. The serving infrastructure must be redesigned with explanation budgets from initial architecture, including pre-computed approximations or model selection favoring inherently interpretable architectures. Teams that treat explainability as optional discover during deployment that their deep ensemble achieves required accuracy but cannot meet regulatory explanation requirements without 5 $\times$ infrastructure cost increases that exceed project budgets. **Fallacy:** ***Achieving one fairness metric guarantees overall system fairness.*** @@ -2318,7 +2318,7 @@ The following key takeaways summarize the essential concepts from this chapter. ::: {.callout-takeaways} -* **Ethics as an Engineering Constraint**: Responsible AI is not a posthoc compliance check. It is an architectural requirement that imposes measurable overhead: fairness monitoring adds 10–20 ms of latency, and SHAP explanations can increase inference compute by 50–1000$\times$. +* **Ethics as an Engineering Constraint**: Responsible AI is not a posthoc compliance check. It is an architectural requirement that imposes measurable overhead: fairness monitoring adds 10–20 ms of latency, and SHAP explanations can increase inference compute by 50–1000 $\times$. * **The Impossibility of "Perfect" Fairness**: Mathematical proofs (Kleinberg et al.) show that multiple fairness criteria (Parity, Odds, Calibration) are mutually exclusive when base rates differ. Fairness is a value-laden engineering decision, not a technical optimization. * **Generative Alignment**: In the LLM era, responsibility shifts from classification parity to *Generative Alignment*. RLHF is the bridge between human preference and model weights, but it is limited by the representativeness of the rating population. * **Governance via System Prompts**: In production fleets, the **System Prompt** is the first line of defense. Managing these prompts across millions of users requires the same version control and CI/CD rigor as model weights (@sec-ops-scale). diff --git a/book/quarto/contents/vol2/robust_ai/robust_ai.qmd b/book/quarto/contents/vol2/robust_ai/robust_ai.qmd index a2f09d9c11..89e96f8272 100644 --- a/book/quarto/contents/vol2/robust_ai/robust_ai.qmd +++ b/book/quarto/contents/vol2/robust_ai/robust_ai.qmd @@ -55,7 +55,7 @@ When traditional software fails, it often does so loudly. A server crashes, an a This silent failure challenge grows more severe as ML systems expand across diverse deployment contexts, from cloud-based services to edge devices and embedded systems, where hardware and software faults directly impact performance and reliability. The increasing complexity of these systems and their deployment in safety-critical applications[^fn-safety-critical] makes robust and fault-tolerant designs essential for maintaining system integrity. -[^fn-safety-critical]: **Safety-Critical Applications**: Systems where failure risks loss of life, property damage, or environmental harm (SIL 3-4 or ASIL D classifications). Nuclear plants, aircraft, and medical devices require 10⁻⁹ failure rates—1000$\times$ stricter than consumer electronics. ML deployment in these domains demands formal verification, extensive testing, and regulatory certification processes spanning years. +[^fn-safety-critical]: **Safety-Critical Applications**: Systems where failure risks loss of life, property damage, or environmental harm (SIL 3-4 or ASIL D classifications). Nuclear plants, aircraft, and medical devices require 10⁻⁹ failure rates—1000 $\times$ stricter than consumer electronics. ML deployment in these domains demands formal verification, extensive testing, and regulatory certification processes spanning years. Building on the adaptive deployment challenges from @sec-edge-intelligence, the security vulnerabilities from @sec-security-privacy, the distributed fault tolerance mechanisms from @sec-fault-tolerance-reliability, and the operational monitoring practices from @sec-ops-scale, we now address end-to-end system reliability. While @sec-fault-tolerance-reliability addressed checkpointing and recovery for training jobs, this chapter extends fault tolerance to the complete ML lifecycle: hardware faults during inference, adversarial inputs in production, and environmental drift over deployment. ML systems operate across diverse domains where systemic failures can have severe consequences ranging from economic disruption to life-threatening situations. These failures include hardware and software faults, malicious inputs such as adversarial attacks and data poisoning, and environmental shifts. @@ -74,7 +74,7 @@ This chapter examines robustness challenges through a unified three-category fra The first category, systemic hardware failures, presents challenges across computing systems including cloud infrastructure, edge devices, and embedded systems. Whether transient[^fn-transient-vs-permanent], permanent, or intermittent, these faults can corrupt computations and degrade system performance. The impact ranges from temporary glitches to complete component failures, requiring detection and mitigation strategies to maintain reliable operation. This hardware-centric perspective extends beyond the algorithmic optimizations of other chapters to address physical layer vulnerabilities. -[^fn-transient-vs-permanent]: **Transient vs Permanent Faults**: Transient faults are temporary disruptions (lasting microseconds to seconds) often caused by cosmic rays or electromagnetic interference, while permanent faults cause lasting damage requiring component replacement. Transient faults are 1000$\times$ more common than permanent faults in modern systems [@baumann2005soft]. +[^fn-transient-vs-permanent]: **Transient vs Permanent Faults**: Transient faults are temporary disruptions (lasting microseconds to seconds) often caused by cosmic rays or electromagnetic interference, while permanent faults cause lasting damage requiring component replacement. Transient faults are 1000 $\times$ more common than permanent faults in modern systems [@baumann2005soft]. Malicious manipulation represents the second category, examining adversarial robustness from an engineering perspective rather than the security-first approach of @sec-security-privacy. While that chapter addresses authentication, access control, and privacy preservation, we focus on maintaining model performance under attack. Adversarial attacks, data poisoning attempts, and prompt injection vulnerabilities can cause models to misclassify inputs or produce unreliable outputs, requiring specialized defensive mechanisms distinct from traditional security measures. @@ -90,7 +90,7 @@ The approaches to achieving robustness vary based on deployment context and syst Despite these contextual differences, the essential characteristics of a robust ML system include fault tolerance, error resilience, and sustained performance. Understanding and addressing these challenges enables engineers to develop reliable ML systems capable of operating effectively in real-world environments. -Robust AI systems require additional computational resources compared to basic implementations, creating tensions with the sustainability principles from @sec-sustainable-ai. Error correction mechanisms consume 12-25% additional memory bandwidth, redundant processing increases energy consumption by 2-3$\times$, and continuous monitoring adds 5-15% computational overhead. These robustness measures also generate additional heat and exacerbate thermal management challenges that constrain deployment density and require enhanced cooling infrastructure. Understanding these sustainability trade-offs enables engineers to make informed decisions about where robustness investments provide the greatest value while minimizing environmental impact. +Robust AI systems require additional computational resources compared to basic implementations, creating tensions with the sustainability principles from @sec-sustainable-ai. Error correction mechanisms consume 12-25% additional memory bandwidth, redundant processing increases energy consumption by 2-3 $\times$, and continuous monitoring adds 5-15% computational overhead. These robustness measures also generate additional heat and exacerbate thermal management challenges that constrain deployment density and require enhanced cooling infrastructure. Understanding these sustainability trade-offs enables engineers to make informed decisions about where robustness investments provide the greatest value while minimizing environmental impact. This chapter systematically examines these multidimensional robustness challenges, exploring detection and mitigation techniques across hardware, algorithmic, and environmental domains. Building on the deployment strategies from edge systems (@sec-edge-intelligence) and connecting with resource efficiency principles from @sec-sustainable-ai, we develop practical approaches that address fault tolerance requirements across all computing environments while considering energy and thermal constraints. The systematic examination of robustness challenges provided here establishes the foundation for building reliable AI systems that maintain performance and safety in real-world deployments, transforming robustness from an afterthought into a core design principle for production systems. @@ -440,7 +440,7 @@ Detection and monitoring form the foundation of any robustness strategy. Hardwar [^fn-mmd]: **Maximum Mean Discrepancy (MMD)**: A kernel-based statistical test that measures the distance between two probability distributions by comparing their embeddings in a reproducing kernel Hilbert space. MMD provides a non-parametric way to detect distribution shift without assumptions about the underlying distributions, making it particularly useful for high-dimensional ML data where traditional statistical tests may fail. -Graceful degradation ensures that systems maintain core functionality even when operating under stress. Rather than catastrophic failure, robust systems exhibit predictable performance reduction that preserves critical capabilities. ECC memory systems recover from single-bit errors with 99.9% success rates while adding 12.5% bandwidth overhead. Model quantization from FP32 to INT8 reduces memory requirements by 75% and inference time by 2-4$\times$, trading 1-3% accuracy for continued operation under resource constraints. Ensemble fallback systems maintain 85-90% of peak performance when primary models fail, with switchover latency under 10 ms. +Graceful degradation ensures that systems maintain core functionality even when operating under stress. Rather than catastrophic failure, robust systems exhibit predictable performance reduction that preserves critical capabilities. ECC memory systems recover from single-bit errors with 99.9% success rates while adding 12.5% bandwidth overhead. Model quantization from FP32 to INT8 reduces memory requirements by 75% and inference time by 2-4 $\times$, trading 1-3% accuracy for continued operation under resource constraints. Ensemble fallback systems maintain 85-90% of peak performance when primary models fail, with switchover latency under 10 ms. Adaptive response enables systems to adjust their behavior based on detected threats or changing conditions. Adaptation might involve activating error correction mechanisms, applying input preprocessing techniques, or dynamically adjusting model parameters. The key principle is that robustness is not static but requires ongoing adjustment to maintain effectiveness. @@ -566,7 +566,7 @@ These manifestations encompass several distinct categories. Common transient fau Modern ML systems require precise understanding of fault rates and their performance implications to make informed engineering decisions. The quantitative analysis of transient faults reveals significant patterns that inform robust system design. -Advanced semiconductor processes exhibit dramatically higher soft error rates. Modern 7 nm processes experience approximately 1000$\times$ higher soft error rates compared to 65 nm nodes due to reduced node capacitance and charge collection efficiency [@baumann2005soft]. For ML accelerators fabricated on advanced processes, this translates to base error rates of approximately 1 error per 10^14 operations, requiring systematic error detection and correction strategies. +Advanced semiconductor processes exhibit dramatically higher soft error rates. Modern 7 nm processes experience approximately 1000 $\times$ higher soft error rates compared to 65 nm nodes due to reduced node capacitance and charge collection efficiency [@baumann2005soft]. For ML accelerators fabricated on advanced processes, this translates to base error rates of approximately 1 error per 10^14 operations, requiring systematic error detection and correction strategies. These theoretical fault rates translate into practical reliability metrics that vary significantly with deployment environment and workload characteristics. Typical AI accelerators demonstrate Mean Time Between Failures (MTBF)[^fn-mtbf] values that differ substantially across deployment contexts: @@ -585,13 +585,13 @@ Memory subsystems represent the most vulnerability-prone components in modern ML Different memory technologies exhibit distinct fault patterns and protection requirements. @tbl-memory-bandwidth-protection compares how ECC overhead impacts effective bandwidth across memory types, from 44.8 GB/s effective on DDR4-3200 to 1,400 GB/s on HBM3: - **DRAM**: Base error rate of 1 per 10^17 bits, dominated by single-bit soft errors. Requires refresh-based error detection and correction. -- **HBM (High Bandwidth Memory)**: 10$\times$ higher error rates due to 3D stacking effects and thermal density. Advanced ECC required for reliable operation. +- **HBM (High Bandwidth Memory)**: 10 $\times$ higher error rates due to 3D stacking effects and thermal density. Advanced ECC required for reliable operation. - **SRAM (Cache)**: Lower soft error rates (1 per 10^19 bits) but higher vulnerability to voltage variations and process variations. - **NVM (Non-Volatile Memory)**: Emerging technologies like 3D XPoint with unique error patterns requiring specialized protection schemes[^fn-nvm-technologies]. -[^fn-nvm-technologies]: **Non-Volatile Memory (NVM) Technologies**: Storage-class memory that bridges DRAM and traditional storage, including Intel's 3D XPoint (Optane) and emerging resistive RAM technologies. Introduced commercially in 2017, NVM provides 1000$\times$ faster access than SSDs while maintaining data persistence, enabling new ML system architectures where models can remain memory-resident across power cycles. +[^fn-nvm-technologies]: **Non-Volatile Memory (NVM) Technologies**: Storage-class memory that bridges DRAM and traditional storage, including Intel's 3D XPoint (Optane) and emerging resistive RAM technologies. Introduced commercially in 2017, NVM provides 1000 $\times$ faster access than SSDs while maintaining data persistence, enabling new ML system architectures where models can remain memory-resident across power cycles. -- **GDDR**: Optimized for bandwidth over reliability, typically 2-3$\times$ higher error rates than standard DRAM. +- **GDDR**: Optimized for bandwidth over reliability, typically 2-3 $\times$ higher error rates than standard DRAM. The choice of memory technology and protection mechanism directly affects available bandwidth for ML workloads: @@ -638,13 +638,13 @@ Timing and logic vulnerabilities create additional pathways for transient faults #### Transient Fault Propagation {#sec-robust-ai-transient-fault-propagation-4bee} -Building on these underlying causes, transient faults can manifest through different mechanisms depending on the affected hardware component. In memory devices like DRAM or SRAM, transient faults often lead to bit flips, where a single bit changes its value from 0 to 1 or vice versa. This can corrupt the stored data or instructions. In logic circuits, transient faults can cause glitches[^fn-glitches] or voltage spikes propagating through the combinational logic[^fn-combinationallogic], resulting in incorrect outputs or control signals. Graphics Processing Units (GPUs)[^fn-gpu-fault-rates] used extensively in ML workloads exhibit significantly higher error rates than traditional CPUs, with studies showing GPU error rates 10-1000$\times$ higher than CPU errors due to their parallel architecture, higher transistor density, and aggressive voltage/frequency scaling. This disparity makes GPU-accelerated AI systems particularly vulnerable to transient faults during training and inference operations. Transient faults can also affect communication channels, causing bit errors or packet losses during data transmission. In distributed AI training systems, network partitions[^fn-network-partitions] occur with measurable frequency - studies of large-scale clusters report partition events affecting 1-10% of nodes daily, with recovery times ranging from seconds to hours depending on the partition type and detection mechanisms. +Building on these underlying causes, transient faults can manifest through different mechanisms depending on the affected hardware component. In memory devices like DRAM or SRAM, transient faults often lead to bit flips, where a single bit changes its value from 0 to 1 or vice versa. This can corrupt the stored data or instructions. In logic circuits, transient faults can cause glitches[^fn-glitches] or voltage spikes propagating through the combinational logic[^fn-combinationallogic], resulting in incorrect outputs or control signals. Graphics Processing Units (GPUs)[^fn-gpu-fault-rates] used extensively in ML workloads exhibit significantly higher error rates than traditional CPUs, with studies showing GPU error rates 10-1000 $\times$ higher than CPU errors due to their parallel architecture, higher transistor density, and aggressive voltage/frequency scaling. This disparity makes GPU-accelerated AI systems particularly vulnerable to transient faults during training and inference operations. Transient faults can also affect communication channels, causing bit errors or packet losses during data transmission. In distributed AI training systems, network partitions[^fn-network-partitions] occur with measurable frequency - studies of large-scale clusters report partition events affecting 1-10% of nodes daily, with recovery times ranging from seconds to hours depending on the partition type and detection mechanisms. [^fn-glitches]: **Glitches**: Momentary deviations in voltage, current, or signal timing that cause transient incorrect operation. First systematically studied in the 1970s as IC complexity grew, glitches now affect 0.01-1% of computations in nanometer-scale circuits. In ML accelerators, a single glitch during matrix multiplication can corrupt thousands of gradient values, potentially causing training divergence or inference errors. [^fn-combinationallogic]: **Combinational Logic**: Digital circuits where outputs depend solely on current inputs, without memory or feedback. Unlike sequential logic, combinational circuits produce deterministic outputs within propagation delay. TPU matrix units rely heavily on combinational logic for parallel multiply-accumulate operations, processing millions of tensor operations per clock cycle. -[^fn-gpu-fault-rates]: **GPU Fault Characteristics**: Graphics processors experience dramatically higher error rates than CPUs due to thousands of simpler cores operating at higher frequencies with aggressive power optimization. NVIDIA's V100 contains 5,120 CUDA cores versus 24-48 cores in server CPUs, creating 100$\times$ more potential failure points. Additionally, GPU memory (HBM2) operates at up to 1.6 TB/s bandwidth in the V100 with minimal error correction, making AI training particularly vulnerable to silent data corruption. +[^fn-gpu-fault-rates]: **GPU Fault Characteristics**: Graphics processors experience dramatically higher error rates than CPUs due to thousands of simpler cores operating at higher frequencies with aggressive power optimization. NVIDIA's V100 contains 5,120 CUDA cores versus 24-48 cores in server CPUs, creating 100 $\times$ more potential failure points. Additionally, GPU memory (HBM2) operates at up to 1.6 TB/s bandwidth in the V100 with minimal error correction, making AI training particularly vulnerable to silent data corruption. [^fn-network-partitions]: **Network Partitions**: Temporary loss of communication between groups of nodes in a distributed system, violating network connectivity assumptions. First studied systematically by Lamport in 1978, partitions affect large-scale ML training where thousands of nodes must synchronize gradients. Modern solutions include gradient compression, asynchronous updates, and Byzantine-fault-tolerant protocols that maintain training progress despite 10-30% node failures. These network disruptions can cause training job failures, parameter synchronization issues, and data inconsistencies that require robust distributed coordination protocols to maintain system reliability. @@ -660,7 +660,7 @@ During the inference phase, transient faults can impact the reliability and trus These vulnerabilities are particularly amplified in resource-constrained environments like TinyML, where limited computational and memory resources exacerbate their impact. One prominent example is Binarized Neural Networks (BNNs) [@courbariaux2016binarized], which represent network weights in single-bit precision to achieve computational efficiency and faster inference times. While this binary representation is advantageous for resource-constrained systems, it also makes BNNs particularly fragile to bit-flip errors. For instance, prior work [@Aygun2021BSBNN] has shown that a two-hidden-layer BNN architecture for a simple task such as MNIST classification suffers performance degradation from 98% test accuracy to 70% when random bit-flipping soft errors are inserted through model weights with a 10% probability. To address these vulnerabilities, techniques like flip-aware training and emerging approaches such as [stochastic computing](https://en.wikipedia.org/wiki/Stochastic_computing)[^fn-stochastic-computing] are being explored to enhance fault tolerance. -[^fn-stochastic-computing]: **Stochastic Computing**: Computation paradigm representing values as probability streams of random bits, enabling arithmetic through simple logic gates. Invented by Gaines in 1969, it offers inherent fault tolerance since bit errors only slightly perturb probabilities. Modern implementations achieve 10-100$\times$ energy efficiency for neural network inference, trading precision for resilience in edge AI applications. +[^fn-stochastic-computing]: **Stochastic Computing**: Computation paradigm representing values as probability streams of random bits, enabling arithmetic through simple logic gates. Invented by Gaines in 1969, it offers inherent fault tolerance since bit errors only slightly perturb probabilities. Modern implementations achieve 10-100 $\times$ energy efficiency for neural network inference, trading precision for resilience in edge AI applications. ### Permanent Faults {#sec-robust-ai-permanent-faults-7dfb} @@ -674,7 +674,7 @@ To illustrate the serious implications of permanent faults, a notable example is The FDIV bug occurred due to an error in the lookup table[^fn-lookup-table] used by the division unit. In rare cases, the processor would fetch an incorrect value, resulting in a slightly less precise result than expected. For instance, when calculating the fraction 4195835/3145727 on a Pentium processor with the FDIV fault, triangular regions highlight where erroneous calculations occurred (@fig-permanent-fault). Ideally, all correct values would round to 1.3338, but the faulty results showed 1.3337, indicating a mistake in the 5th digit. -[^fn-lookup-table]: **Lookup Table (LUT)**: Data structure replacing runtime computation with pre-computed array indexing, trading memory for speed. FPGAs use LUTs as fundamental building blocks, with modern devices containing millions of 6-input LUTs. In ML accelerators, LUT-based activation functions achieve 10$\times$ lower latency than direct computation while enabling efficient quantized inference. +[^fn-lookup-table]: **Lookup Table (LUT)**: Data structure replacing runtime computation with pre-computed array indexing, trading memory for speed. FPGAs use LUTs as fundamental building blocks, with modern devices containing millions of 6-input LUTs. In ML accelerators, LUT-based activation functions achieve 10 $\times$ lower latency than direct computation while enabling efficient quantized inference. Although the error was small, it could compound across many operations, affecting results in precision-critical applications such as scientific simulations, financial calculations, and computer-aided design. The bug ultimately led to incorrect outcomes in these domains and underscored the severe consequences permanent faults can have. @@ -1455,13 +1455,13 @@ Label flipping attacks modify the labels of training examples to introduce incor Backdoor attacks inject training samples with specific trigger patterns that cause models to exhibit attacker-controlled behavior when the trigger is present in test inputs [@gu2017badnets]. Studies show that inserting backdoor triggers in just 1% of training data achieves 99.5% attack success rates on trigger-bearing test inputs. The model performs normally on clean inputs but consistently misclassifies inputs containing the backdoor trigger, with clean accuracy typically dropping less than 1%. -Gradient-based poisoning crafts training samples that appear benign but cause gradient updates during training to move the model toward attacker objectives [@shafahi2018poison]. These attacks require precise optimization but can be devastating: poisoning 50 crafted images in CIFAR-10 (0.1% of training data) achieves target misclassification rates above 70%. The computational cost is significant, requiring 15-20$\times$ more training time to generate optimal poisoning samples, but the attack remains undetectable through visual inspection. +Gradient-based poisoning crafts training samples that appear benign but cause gradient updates during training to move the model toward attacker objectives [@shafahi2018poison]. These attacks require precise optimization but can be devastating: poisoning 50 crafted images in CIFAR-10 (0.1% of training data) achieves target misclassification rates above 70%. The computational cost is significant, requiring 15-20 $\times$ more training time to generate optimal poisoning samples, but the attack remains undetectable through visual inspection. ### Detection and Mitigation Strategies {#sec-robust-ai-detection-mitigation-strategies-8dbe} Robust AI systems employ multiple defense mechanisms against input-level attacks, following the detection, graceful degradation, and adaptive response principles established in our unified framework. -Input sanitization applies preprocessing techniques to remove or reduce adversarial perturbations before they reach the model. JPEG compression with quality factor 75% neutralizes 60-80% of adversarial examples while reducing clean accuracy by only 1-2%. Image denoising with Gaussian filters (σ = 0.5) blocks 45% of FGSM attacks but requires careful tuning to avoid degrading legitimate inputs. Geometric transformations like random rotations (±15°) and scaling (0.9-1.1$\times$) provide 30-50% defense effectiveness with minimal clean accuracy loss. +Input sanitization applies preprocessing techniques to remove or reduce adversarial perturbations before they reach the model. JPEG compression with quality factor 75% neutralizes 60-80% of adversarial examples while reducing clean accuracy by only 1-2%. Image denoising with Gaussian filters (σ = 0.5) blocks 45% of FGSM attacks but requires careful tuning to avoid degrading legitimate inputs. Geometric transformations like random rotations (±15°) and scaling (0.9-1.1 $\times$) provide 30-50% defense effectiveness with minimal clean accuracy loss. Adversarial training [@madry2017towards] incorporates adversarial examples into the training process, teaching models to maintain correct predictions in the presence of adversarial perturbations. PGD adversarial training on CIFAR-10 achieves 87% robust accuracy against ε = 8/255 attacks compared to 0% for undefended models, though clean accuracy drops from 95% to 84%. Beyond the accuracy penalty, adversarial training imposes a *robustness compute penalty* that fundamentally changes the economics of model development. @@ -1478,12 +1478,12 @@ where $K_{pgd}$ is the number of Projected Gradient Descent (PGD) steps used to * **Adversarial Step (PGD-7)**: * 7 iterations of (Forward + Backward) to find the attack perturbation. * 1 final (Forward + Backward) to update weights on the attacked input. - * Total: ~8$\times$ computational cost. + * Total: ~8 $\times$ computational cost. -**Conclusion**: Achieving state-of-the-art robustness requires **8-10$\times$ more compute** per epoch. For a large model like **Llama-3 (Lighthouse)**, this penalty is often prohibitive, forcing teams to rely on post-hoc defenses (e.g., input filtering) instead of intrinsic robustness. +**Conclusion**: Achieving state-of-the-art robustness requires **8-10 $\times$ more compute** per epoch. For a large model like **Llama-3 (Lighthouse)**, this penalty is often prohibitive, forcing teams to rely on post-hoc defenses (e.g., input filtering) instead of intrinsic robustness. ::: -Training time increases 6-10$\times$ due to adversarial example generation during each epoch, requiring specialized hardware acceleration for practical implementation. +Training time increases 6-10 $\times$ due to adversarial example generation during each epoch, requiring specialized hardware acceleration for practical implementation. **The Robustness-Accuracy Tradeoff** @@ -1491,7 +1491,7 @@ Robustness is not free. A fundamental trade-off exists between accuracy on clean This occurs because standard training exploits high-frequency features (e.g., texture patterns invisible to humans) that are predictive but brittle. Adversarial training forces the model to ignore these features and rely only on robust, semantic features (e.g., shape), which are harder to learn and less discriminative. Engineers must explicitly decide how much clean accuracy they are willing to sacrifice for security. -Certified defenses provide mathematical guarantees about model robustness within specified perturbation bounds [@cohen2019certified]. Randomized smoothing achieves 67% certified accuracy on ImageNet for ℓ2 perturbations with σ = 0.5, compared to 76% clean accuracy. The certification radius increases to ε = 1.0 for 54% of test inputs, providing provable robustness guarantees. However, inference time increases 100-1000$\times$ due to Monte Carlo sampling requirements (typically 1,000 samples per prediction). +Certified defenses provide mathematical guarantees about model robustness within specified perturbation bounds [@cohen2019certified]. Randomized smoothing achieves 67% certified accuracy on ImageNet for ℓ2 perturbations with σ = 0.5, compared to 76% clean accuracy. The certification radius increases to ε = 1.0 for 54% of test inputs, providing provable robustness guarantees. However, inference time increases 100-1000 $\times$ due to Monte Carlo sampling requirements (typically 1,000 samples per prediction). Ensemble methods leverage multiple models or detection mechanisms to identify and filter adversarial inputs [@tramer2017ensemble]. Ensembles of 5 independently trained models achieve 94% detection rates for adversarial examples using prediction entropy thresholds (τ = 1.5), with false positive rates below 2% on clean data. Computational overhead scales linearly with ensemble size, requiring $5\times$ inference time and memory for the 5-model ensemble, making real-time deployment challenging. @@ -1863,7 +1863,7 @@ This vulnerability stems from several characteristics of neural network learning [^fn-nn-learning]: **Neural Network Learning Mechanisms**: Gradient-based optimization of weights through backpropagation, forming nonlinear decision boundaries in high-dimensional feature spaces. Universal approximation theorem (Hornik, 1989) proves networks with sufficient hidden units can approximate any continuous function. Training dynamics include saddle points, sharp minima (poor generalization), and implicit regularization from SGD noise. -[^fn-curse-of-dimensionality]: **Curse of Dimensionality in Adversarial Settings**: In high-dimensional spaces (e.g., 224$\times$224$\times$3 = 150,528 dimensions for ImageNet images), tiny perturbations accumulate significantly. With ε=0.01 per dimension, total perturbation magnitude can reach √150,528 $\times$ 0.01 ≈ 3.88, enough to alter model predictions while remaining imperceptible to humans using complete visual perception. Non-linear decision boundaries create complex separations that make models sensitive to precise input modifications. +[^fn-curse-of-dimensionality]: **Curse of Dimensionality in Adversarial Settings**: In high-dimensional spaces (e.g., 224 $\times$ 224 $\times$ 3 = 150,528 dimensions for ImageNet images), tiny perturbations accumulate significantly. With ε=0.01 per dimension, total perturbation magnitude can reach √150,528 $\times$ 0.01 ≈ 3.88, enough to alter model predictions while remaining imperceptible to humans using complete visual perception. Non-linear decision boundaries create complex separations that make models sensitive to precise input modifications. This deep understanding of why adversarial examples exist is crucial for developing effective defenses. The vulnerability reflects core properties of how neural networks represent and process information in high-dimensional spaces, rather than being merely a software bug or training artifact. Neural networks create complex, non-linear decision boundaries in high-dimensional feature spaces, making them inherently vulnerable to adversarial perturbations that exploit these geometric properties. @@ -2612,7 +2612,7 @@ Adversarial training is a technique that involves augmenting the training data w Adversarial training provides improved robustness but comes with significant computational overhead that must be carefully managed in production systems. -Training time increases 3-10$\times$ due to adversarial example generation during each training step. On-the-fly adversarial example generation requires additional forward and backward passes through the model, substantially increasing computational requirements. Memory requirements increase 2-3$\times$ for storing both clean and adversarial examples, along with gradients computed during attack generation. Specialized infrastructure may be needed for efficient adversarial example generation, particularly when using iterative attacks like PGD that require multiple optimization steps. +Training time increases 3-10 $\times$ due to adversarial example generation during each training step. On-the-fly adversarial example generation requires additional forward and backward passes through the model, substantially increasing computational requirements. Memory requirements increase 2-3 $\times$ for storing both clean and adversarial examples, along with gradients computed during attack generation. Specialized infrastructure may be needed for efficient adversarial example generation, particularly when using iterative attacks like PGD that require multiple optimization steps. Robust models typically sacrifice 2-8% clean accuracy for improved adversarial robustness, representing a fundamental trade-off in the robust optimization objective. Inference time may increase if ensemble methods or uncertainty estimation techniques are integrated with adversarial training. Model size often increases with robustness-enhancing architectural modifications, such as wider networks or additional normalization layers that improve gradient stability. @@ -2640,7 +2640,7 @@ def adversarial_training_step(model, data, labels, epsilon=0.1): ``` ::: -The implementation in @lst-adversarial-training generates adversarial examples on-the-fly during training by computing gradients with respect to input data (line 2190), applying the sign function to extract perturbation direction (line 2196), and mixing the resulting adversarial examples with clean training data (lines 2199-2200). The `torch.clamp()` operation ensures pixel values remain valid, while the final concatenation doubles the effective batch size by combining clean and adversarial examples. This approach requires careful tuning of the perturbation budget $\epsilon$ and typically increases training time by 2-3$\times$ compared to standard training [@shafahi2019adversarial]. +The implementation in @lst-adversarial-training generates adversarial examples on-the-fly during training by computing gradients with respect to input data (line 2190), applying the sign function to extract perturbation direction (line 2196), and mixing the resulting adversarial examples with clean training data (lines 2199-2200). The `torch.clamp()` operation ensures pixel values remain valid, while the final concatenation doubles the effective batch size by combining clean and adversarial examples. This approach requires careful tuning of the perturbation budget $\epsilon$ and typically increases training time by 2-3 $\times$ compared to standard training [@shafahi2019adversarial]. Production deployment patterns, MLOps pipeline integration, and monitoring strategies for robust ML systems require careful coordination with operational workflows, while distributed robustness coordination and fault tolerance at scale are addressed through distributed training techniques that synchronize updates across multiple nodes. @@ -3776,7 +3776,7 @@ Despite their high accuracy, hardware-based fault injection methods have several First, cost is a major barrier. Both FPGA-based and beam testing[^fn-beam_testing] approaches require specialized hardware and facilities, which can be expensive to set up and maintain. This makes them less accessible to research groups with limited funding or infrastructure. -[^fn-beam_testing]: **Beam Testing**: Accelerated reliability testing exposing hardware to controlled particle radiation (protons, neutrons, heavy ions) to evaluate soft error susceptibility. Facilities like Los Alamos Neutron Science Center provide 10⁹$\times$ acceleration over natural cosmic ray rates. Required for aerospace and automotive AI certification, beam testing revealed that modern 7nm chips experience 10$\times$ higher soft error rates than 28nm predecessors. +[^fn-beam_testing]: **Beam Testing**: Accelerated reliability testing exposing hardware to controlled particle radiation (protons, neutrons, heavy ions) to evaluate soft error susceptibility. Facilities like Los Alamos Neutron Science Center provide 10⁹ $\times$ acceleration over natural cosmic ray rates. Required for aerospace and automotive AI certification, beam testing revealed that modern 7nm chips experience 10 $\times$ higher soft error rates than 28nm predecessors. Second, these methods face challenges in scalability. Injecting faults and collecting data directly on hardware is time-consuming, which limits the number of experiments that can be run in a reasonable timeframe. This is especially restrictive when analyzing large ML systems or performing statistical evaluations across many fault scenarios. @@ -4122,7 +4122,7 @@ Teams often apply robustness techniques developed for specific threats without u **Fallacy: Different failure modes operate independently and can be addressed in isolation.** -This assumption overlooks the complex interactions between different fault types that can create compound vulnerabilities exceeding the sum of individual threats. Real-world failures often involve cascading effects where one vulnerability enables or amplifies others. Hardware bit flips in model weights can create new adversarial vulnerabilities, enabling attackers to achieve 95% attack success rates compared to 20% on uncorrupted models. Conversely, adversarial training increases model complexity by 2-3$\times$, raising hardware fault probability. Environmental-software interactions compound when distribution shift goes undetected due to monitoring bugs, causing 40% accuracy degradation over 3-6 months while faulty monitors report normal operation, resulting in 10$\times$ higher recovery costs than early detection. Adversaries can amplify natural drift by injecting just 0.1% poisoned samples aligned with drift patterns, causing 5$\times$ faster degradation while evading detection calibrated for single threats. In autonomous vehicles, cosmic ray bit flips corrupting perception weights, adversarial road markings exploiting these corruptions, and seasonal weather shifts combine to produce 85% stop sign misclassification under specific conditions. Each individual threat alone causes only 15-20% degradation. Robust AI systems must consider threat interactions through systematic failure mode analysis, cross-domain testing, and defense strategies that account for cascading failures rather than treating each threat in isolation. +This assumption overlooks the complex interactions between different fault types that can create compound vulnerabilities exceeding the sum of individual threats. Real-world failures often involve cascading effects where one vulnerability enables or amplifies others. Hardware bit flips in model weights can create new adversarial vulnerabilities, enabling attackers to achieve 95% attack success rates compared to 20% on uncorrupted models. Conversely, adversarial training increases model complexity by 2-3 $\times$, raising hardware fault probability. Environmental-software interactions compound when distribution shift goes undetected due to monitoring bugs, causing 40% accuracy degradation over 3-6 months while faulty monitors report normal operation, resulting in 10 $\times$ higher recovery costs than early detection. Adversaries can amplify natural drift by injecting just 0.1% poisoned samples aligned with drift patterns, causing 5 $\times$ faster degradation while evading detection calibrated for single threats. In autonomous vehicles, cosmic ray bit flips corrupting perception weights, adversarial road markings exploiting these corruptions, and seasonal weather shifts combine to produce 85% stop sign misclassification under specific conditions. Each individual threat alone causes only 15-20% degradation. Robust AI systems must consider threat interactions through systematic failure mode analysis, cross-domain testing, and defense strategies that account for cascading failures rather than treating each threat in isolation. ## Summary {#sec-robust-ai-summary-a274} @@ -4133,7 +4133,7 @@ We analyzed the growing "Reliability Crisis" of sub-5nm hardware, where the phys ::: {.callout-takeaways} * **The Unreliable Substrate**: As we scale to sub-5nm process nodes, Silent Data Corruption (SDC) rates are rising. Hardware reliability is no longer a given; it must be managed by the ML system layer through algorithmic fault tolerance (e.g., gradient checksums). -* **Robustness vs. Accuracy**: Building resilient systems requires an explicit trade-off. Techniques like adversarial training or certified defenses can improve security but often degrade clean accuracy by 10–15% and increase compute costs by 2–10$\times$. +* **Robustness vs. Accuracy**: Building resilient systems requires an explicit trade-off. Techniques like adversarial training or certified defenses can improve security but often degrade clean accuracy by 10–15% and increase compute costs by 2–10 $\times$. * **Semantic Robustness in LLMs**: Generative AI fails not just through misclassification, but through "hallucinations." Robustness at this level requires **Uncertainty Quantification (UQ)**—using entropy and self-consistency to detect when a model is guessing. * **The Drift Detection Arsenal**: Environmental shift is inevitable. Robust fleets use statistical distance metrics (MMD, PSI) to detect when the real world has diverged from the training assumptions, triggering automated retraining in the MLOps pipeline. * **Compound Threats**: Failures rarely happen in isolation. A hardware bit-flip can create a new adversarial vulnerability, and a natural distribution shift can mask a subtle data poisoning attack. Defense must be multi-layered and systematic. diff --git a/book/quarto/contents/vol2/security_privacy/security_privacy.qmd b/book/quarto/contents/vol2/security_privacy/security_privacy.qmd index 6c0c9dcf12..211504f431 100644 --- a/book/quarto/contents/vol2/security_privacy/security_privacy.qmd +++ b/book/quarto/contents/vol2/security_privacy/security_privacy.qmd @@ -2092,7 +2092,7 @@ $$ \tilde{f}(D) = f(D) + \text{Lap}\left(\frac{\Delta f}{\epsilon}\right) $$ -The Laplace distribution has density $p(x) = \frac{\epsilon}{2\Delta f}\exp\left(-\frac{\epsilon|x|}{\Delta f}\right)$, with scale parameter $b = \Delta f/\epsilon$. The noise magnitude scales inversely with $\epsilon$: stronger privacy ($\epsilon = 0.1$) requires 10$\times$ more noise than moderate privacy ($\epsilon = 1.0$). +The Laplace distribution has density $p(x) = \frac{\epsilon}{2\Delta f}\exp\left(-\frac{\epsilon|x|}{\Delta f}\right)$, with scale parameter $b = \Delta f/\epsilon$. The noise magnitude scales inversely with $\epsilon$: stronger privacy ($\epsilon = 0.1$) requires 10 $\times$ more noise than moderate privacy ($\epsilon = 1.0$). **Derivation of Laplace Mechanism Privacy Guarantee**: To prove that the Laplace mechanism achieves $\epsilon$-DP, consider the ratio of output probabilities on adjacent datasets $D$ and $D'$. Let $f(D) = v$ and $f(D') = v'$, where $|v - v'| \leq \Delta f$ by the sensitivity bound. For any output $y$, the probability ratio is: $$ @@ -2142,7 +2142,7 @@ $$ \sigma \geq \frac{C \sqrt{2 \ln(1.25/\delta)}}{\epsilon} $$ * **Strong Privacy ($\epsilon=1.0$)**: $\sigma \approx 1.0 \times \sqrt{2 \times 11.7} / 1.0 \approx \mathbf{4.8}$ * **Weak Privacy ($\epsilon=10.0$)**: $\sigma \approx 1.0 \times \sqrt{2 \times 11.7} / 10.0 \approx \mathbf{0.48}$ -**Conclusion**: Achieving $\epsilon=1.0$ requires adding noise that is nearly **5$\times$ larger** than the signal (gradient norm 1.0). This can degrade accuracy by 5-10% unless you train for significantly longer or use much larger batch sizes to average out the noise. +**Conclusion**: Achieving $\epsilon=1.0$ requires adding noise that is nearly **5 $\times$ larger** than the signal (gradient norm 1.0). This can degrade accuracy by 5-10% unless you train for significantly longer or use much larger batch sizes to average out the noise. ::: The noise scale must satisfy $\sigma \geq \frac{\Delta_2 f}{\epsilon}\sqrt{2\ln(1.25/\delta)}$ to achieve $(\epsilon, \delta)$-DP. For typical ML hyperparameters ($\epsilon=1, \delta=10^{-7}$), this requires $\sigma \approx 4.45 \cdot \Delta_2 f$. @@ -2194,7 +2194,7 @@ $$ \epsilon = \varepsilon + \frac{\ln(1/\delta)}{\alpha - 1} $$ -optimized over $\alpha$. For typical ML workloads, RDP provides 2-3$\times$ tighter bounds than moments accountant, enabling longer training with fixed privacy budget. +optimized over $\alpha$. For typical ML workloads, RDP provides 2-3 $\times$ tighter bounds than moments accountant, enabling longer training with fixed privacy budget. **Practical Privacy Budget Example: DP-SGD for Image Classification** @@ -2294,7 +2294,7 @@ Quantify the maximum acceptable accuracy degradation before deployment. For safe **Criterion 5: Computational Budget** -DP training typically requires 2-5$\times$ more compute than non-private training due to per-sample gradient computation and larger batch sizes needed to overcome noise. If computational resources are constrained, this overhead may be prohibitive. Cloud deployments can scale compute, but edge training scenarios may find DP impractical. +DP training typically requires 2-5 $\times$ more compute than non-private training due to per-sample gradient computation and larger batch sizes needed to overcome noise. If computational resources are constrained, this overhead may be prohibitive. Cloud deployments can scale compute, but edge training scenarios may find DP impractical. **Decision Matrix Summary** @@ -2430,7 +2430,7 @@ $$ This property supports privacy-preserving computation in untrusted environments, such as cloud inference over sensitive health or financial records. The computational cost of HE remains high, making it more suitable for fixed-function models and low-latency batch tasks. SMPC[^fn-smpc-overhead], by contrast, distributes the computation across multiple parties such that no single party learns the complete input or output. This is particularly useful in joint training across institutions with strict data-use policies, such as hospitals or banks[^fn-smpc-collaborative]. -[^fn-smpc-overhead]: **SMPC Performance Overhead**: Secure multi-party computation incurs 1000-10,000$\times$ computational overhead through cryptographic operations (secret sharing, garbled circuits). GPU-millisecond inference becomes SMPC-hours, limiting applications to small models. Research on GPU-accelerated SMPC and hybrid approaches (SMPC for sensitive layers only) aims to reduce overhead to practical 10-100$\times$ for production deployment. +[^fn-smpc-overhead]: **SMPC Performance Overhead**: Secure multi-party computation incurs 1000-10,000 $\times$ computational overhead through cryptographic operations (secret sharing, garbled circuits). GPU-millisecond inference becomes SMPC-hours, limiting applications to small models. Research on GPU-accelerated SMPC and hybrid approaches (SMPC for sensitive layers only) aims to reduce overhead to practical 10-100 $\times$ for production deployment. [^fn-smpc-collaborative]: **Secure Multi-Party Computation (SMPC)**: Cryptographic framework enabling multiple parties to jointly compute functions over their private inputs without revealing those inputs, first formalized in 1982 by Andrew Yao [@yao1982protocols]. Today's implementations allow hospitals to collaboratively train medical AI models without sharing patient records, achieving 99%+ accuracy while maintaining strict privacy compliance. @@ -2528,7 +2528,7 @@ Input validation is the first line of defense at runtime. It ensures that incomi Machine learning models, unlike traditional rule-based systems, often do not fail safely. Small, carefully chosen changes to input data can cause models to make high-confidence but incorrect predictions. Input validation helps detect and reject such inputs early in the pipeline [@goodfellow2015explaining]. -Validation techniques range from low-level checks (e.g., input size, type, and value ranges) to semantic filters (e.g., verifying whether an image contains a recognizable object or whether a voice recording includes speech). For example, a facial recognition system might validate that the uploaded image is within a certain resolution range (e.g., 224$\times$224 to 1024$\times$1024 pixels), contains RGB channels, and passes a lightweight face detection filter. This prevents inputs like blank images, text screenshots, or synthetic adversarial patterns from reaching the model. Similarly, a voice assistant might require that incoming audio files be between 1 and 5 seconds long, have a valid sampling rate (e.g., 16kHz), and contain detectable human speech using a speech activity detector (SAD)[^fn-speech-activity-detector]. This ensures that empty recordings, music clips, or noise bursts are filtered before model inference. +Validation techniques range from low-level checks (e.g., input size, type, and value ranges) to semantic filters (e.g., verifying whether an image contains a recognizable object or whether a voice recording includes speech). For example, a facial recognition system might validate that the uploaded image is within a certain resolution range (e.g., 224 $\times$ 224 to 1024 $\times$ 1024 pixels), contains RGB channels, and passes a lightweight face detection filter. This prevents inputs like blank images, text screenshots, or synthetic adversarial patterns from reaching the model. Similarly, a voice assistant might require that incoming audio files be between 1 and 5 seconds long, have a valid sampling rate (e.g., 16kHz), and contain detectable human speech using a speech activity detector (SAD)[^fn-speech-activity-detector]. This ensures that empty recordings, music clips, or noise bursts are filtered before model inference. [^fn-speech-activity-detector]: **Speech Activity Detector (SAD)**: Algorithm that distinguishes speech from silence, noise, or music in audio streams, essential for voice interfaces since the 1990s. Modern neural SADs achieve 95%+ accuracy and operate in <10ms latency, enabling real-time filtering before expensive speech recognition processing. @@ -2650,7 +2650,7 @@ Hardware security mechanisms introduce measurable overhead that must be factored Security features scale differently than computational resources. TEE memory limitations constrain model size regardless of available system memory. A quantized ResNet-18 model (47MB) can operate within ARM TrustZone constraints, while ResNet-50 (176MB) requires careful memory management or model partitioning. These constraints create architectural decisions that must be made early in system design. -Different threat models and protection levels require quantitative trade-off analysis. For ML workloads requiring cryptographic verification, AES-256 operations add 0.1-0.5ms per inference depending on model size and hardware acceleration availability. Homomorphic encryption operations impose 100-100,000$\times$ computational overhead, with fully homomorphic encryption (FHE) at the higher end and somewhat homomorphic encryption (SHE) at the lower end, making them viable only for small models or offline scenarios where strong privacy guarantees justify the performance cost. +Different threat models and protection levels require quantitative trade-off analysis. For ML workloads requiring cryptographic verification, AES-256 operations add 0.1-0.5ms per inference depending on model size and hardware acceleration availability. Homomorphic encryption operations impose 100-100,000 $\times$ computational overhead, with fully homomorphic encryption (FHE) at the higher end and somewhat homomorphic encryption (SHE) at the lower end, making them viable only for small models or offline scenarios where strong privacy guarantees justify the performance cost. #### Trusted Execution Environments {#sec-security-privacy-trusted-execution-environments-80ed} @@ -2672,7 +2672,7 @@ Several commercial platforms provide TEE functionality tailored for different de [^fn-trustzone-adoption]: **ARM TrustZone**: Introduced in 2004, TrustZone now ships in 95% of ARM processors, protecting over 5 billion mobile devices. Despite its ubiquity, many devices underutilize TrustZone—studies show only 20-30% of Android devices implement meaningful secure world applications beyond basic key storage. -[^fn-intel-sgx-limits]: **Intel SGX Constraints**: SGX enclaves are limited to approximately 128MB of protected memory (EPC) on most consumer processors, though enterprise variants support up to 512MB or 1GB, with cache misses causing 100$\times$ performance penalties. For ML workloads, a ResNet-50 requires approximately 98MB for weights alone in FP32 format (25.6M parameters $\times$ 4 bytes), consuming 77% of SGX EPC before any intermediate activations. Inference latency increases from 5ms to 150ms when model exceeds EPC capacity. This makes SGX unsuitable for large ML models but effective for protecting cryptographic keys and small inference models under 10MB. +[^fn-intel-sgx-limits]: **Intel SGX Constraints**: SGX enclaves are limited to approximately 128MB of protected memory (EPC) on most consumer processors, though enterprise variants support up to 512MB or 1GB, with cache misses causing 100 $\times$ performance penalties. For ML workloads, a ResNet-50 requires approximately 98MB for weights alone in FP32 format (25.6M parameters $\times$ 4 bytes), consuming 77% of SGX EPC before any intermediate activations. Inference latency increases from 5ms to 150ms when model exceeds EPC capacity. This makes SGX unsuitable for large ML models but effective for protecting cryptographic keys and small inference models under 10MB. @fig-enclave illustrates a practical secure enclave architecture integrated into a system-on-chip (SoC) design. The enclave includes a dedicated processor, an AES engine, a true random number generator (TRNG), a public key accelerator (PKA), and a secure I2C interface to nonvolatile storage. These components operate in isolation from the main application processor and memory subsystem. A memory protection engine enforces access control, while cryptographic operations such as NAND flash encryption are handled internally using enclave-managed keys. By physically separating secure execution and key management from the main system, this architecture limits the impact of system-level compromises and establishes hardware-enforced trust. @@ -3088,7 +3088,7 @@ To support practical decision making, @tbl-defense-selection-framework maps comm | **(Multi-tenant platform)** | backdoor injection, gradient leakage | - Differential Privacy (DP-SGD, ε ≈ 1-10) - Gradient verification and anomaly detection - Secure aggregation (if federated) - Model watermarking for IP protection | gradient verification adds 10-15% compute overhead; federated aggregation requires secure communication protocols | | **Public-Facing LLM** | Prompt injection, | - Input sanitization (prompt filtering) | Aggressive filtering may block 5-10% of | | **(Chatbot/API)** | data extraction (training leakage), abuse/overuse | - Output monitoring (PII detection) - Rate limiting (per-user quotas) - Response watermarking - Confidence thresholding (abstention) | legitimate requests; response time increases 50-100ms for content filtering; watermarking may be detectable by sophisticated users | -| **Multi-Party ML** | Data sharing restrictions, | - Federated Learning (no raw data sharing) | Communication overhead: 10-100$\times$ more rounds | +| **Multi-Party ML** | Data sharing restrictions, | - Federated Learning (no raw data sharing) | Communication overhead: 10-100 $\times$ more rounds | | **(Cross-organizational** | honest-but-curious participants, | - SMPC for secure aggregation | than centralized training; SMPC adds 1000x+ | | **training)** | privacy compliance (GDPR) | - Differential Privacy (ε ≤ 1) - Homomorphic Encryption (for sensitive ops) | compute cost; accuracy may degrade 5-15%; requires legal agreements for liability | | **Critical Infrastructure** | Supply chain compromise, | - Hardware attestation (TPM/PUF) | Development cost: 6-18 months additional | diff --git a/book/quarto/contents/vol2/storage/storage.qmd b/book/quarto/contents/vol2/storage/storage.qmd index 8acfeac3fe..a36347a626 100644 --- a/book/quarto/contents/vol2/storage/storage.qmd +++ b/book/quarto/contents/vol2/storage/storage.qmd @@ -114,9 +114,9 @@ ML systems extend this hierarchy with two critical additions: GPU High Bandwidth | **Object Storage** | Unlimited | 100 GB/s aggregate | ~50 ms | ~0.02 | | **Archive/Cold Storage** | Unlimited | 1 GB/s | Minutes to hours | ~0.004 | -: **Extended Memory Hierarchy for ML Systems**: GPU HBM delivers 3.35 TB/s at ~10 ns latency but costs $15/GB, while object storage offers unlimited capacity at $0.02/GB with 50 ms latency. This 300,000$\times$ bandwidth gap between HBM and storage drives the need for sophisticated prefetching and caching strategies. {#tbl-storage-hierarchy} +: **Extended Memory Hierarchy for ML Systems**: GPU HBM delivers 3.35 TB/s at ~10 ns latency but costs $15/GB, while object storage offers unlimited capacity at $0.02/GB with 50 ms latency. This 300,000 $\times$ bandwidth gap between HBM and storage drives the need for sophisticated prefetching and caching strategies. {#tbl-storage-hierarchy} -Looking at the bandwidth column, GPU HBM delivers `{python} h100_bw_tbs` TB/s, roughly 17$\times$ faster than host DRAM and 130$\times$ faster than the fastest local NVMe drives. This disparity creates the central challenge of ML storage systems: keeping accelerators fed with data at rates that prevent them from idling. +Looking at the bandwidth column, GPU HBM delivers `{python} h100_bw_tbs` TB/s, roughly 17 $\times$ faster than host DRAM and 130 $\times$ faster than the fastest local NVMe drives. This disparity creates the central challenge of ML storage systems: keeping accelerators fed with data at rates that prevent them from idling. ### High Bandwidth Flash (HBF): Blurring Storage and Memory {#sec-storage-high-bandwidth-flash-hbf-blurring-storage-memory-9bea} @@ -168,7 +168,7 @@ This architecture treats Flash not as persistent storage, but as a "cold memory" These bandwidth specifications become concrete when we trace data flow through a single training iteration. Consider what happens when an H100 GPU processes a training batch. At `{python} h100_fp8_tflops` TFLOPS[^fn-tflops] of FP16 compute capability, the GPU can perform approximately 2 quadrillion floating-point operations per second. A typical transformer forward pass requires roughly 6 FLOPs per parameter per token. For a 7 billion parameter model processing 2048-token sequences with a batch size of 32, each forward pass involves: -[^fn-tflops]: **TFLOPS (Tera Floating-Point Operations Per Second)**: A measure of computational throughput representing one trillion ($10^{12}$) floating-point operations per second. The H100's `{python} h100_fp8_tflops` TFLOPS uses FP16 (16-bit floating-point) precision with sparsity optimization; dense FP16 achieves `{python} h100_fp16_tflops` TFLOPS, and FP32 (32-bit) achieves 67 TFLOPS. This 15$\times$ difference between FP16 and FP32 explains why ML training increasingly uses mixed-precision techniques, performing bulk computation in FP16 while maintaining FP32 precision for numerically sensitive operations like loss accumulation. +[^fn-tflops]: **TFLOPS (Tera Floating-Point Operations Per Second)**: A measure of computational throughput representing one trillion ($10^{12}$) floating-point operations per second. The H100's `{python} h100_fp8_tflops` TFLOPS uses FP16 (16-bit floating-point) precision with sparsity optimization; dense FP16 achieves `{python} h100_fp16_tflops` TFLOPS, and FP32 (32-bit) achieves 67 TFLOPS. This 15 $\times$ difference between FP16 and FP32 explains why ML training increasingly uses mixed-precision techniques, performing bulk computation in FP16 while maintaining FP32 precision for numerically sensitive operations like loss accumulation. $$\text{FLOPs per batch} = 6 \times 7 \times 10^9 \times 2048 \times 32 \approx 2.75 \times 10^{15}$$ @@ -196,7 +196,7 @@ Traditional storage system design optimizes for workloads with specific characte | **Read/write ratio** | Balanced | Phase-dependent (100:1 to 1:0) | | **Locality** | Strong temporal locality | No locality (uniform sampling) | -: **ML Workloads Invert Traditional Storage Assumptions**: Where databases optimize for random IOPS with cacheable working sets, ML training streams sequentially through datasets that exceed all cache levels. This fundamental mismatch explains why enterprise storage systems underperform for ML by 10$\times$ or more. {#tbl-storage-assumptions} +: **ML Workloads Invert Traditional Storage Assumptions**: Where databases optimize for random IOPS with cacheable working sets, ML training streams sequentially through datasets that exceed all cache levels. This fundamental mismatch explains why enterprise storage systems underperform for ML by 10 $\times$ or more. {#tbl-storage-assumptions} ### Access Pattern Analysis {#sec-storage-access-pattern-analysis-3683} @@ -299,7 +299,7 @@ Consider training a ResNet-50 [@he2016resnet] model on ImageNet [@deng2009imagen - $N_{GPUs} = 256$ H100 GPUs - $U_{target} = 0.80$ (80% utilization) -- Images are 224$\times$224 RGB after resize, but stored as JPEG at ~150 KB average +- Images are 224 $\times$ 224 RGB after resize, but stored as JPEG at ~150 KB average - Batch size per GPU: 256 images - Target iteration time: 200 ms (achievable with optimized training) @@ -443,7 +443,7 @@ More than 63% of training steps will experience at least one slow storage access **Tail latency sources** include garbage collection pauses (JVM-based storage systems like HDFS NameNode can stall for seconds during GC), disk queue depth (when too many concurrent requests hit one disk, queue wait time dominates), network congestion (shared network fabric experiences transient congestion), background maintenance (compaction, replication, and verification compete with foreground requests), and resource contention (multiple tenants sharing storage create interference). -**Mitigation strategies** include hedged requests (issue redundant reads, take first response; tradeoff: 2$\times$ read amplification), backup requests (issue second request if first is slow; tradeoff: lower amplification, higher complexity), selective replica choice (route to least-loaded replica; tradeoff: requires load monitoring), request cancellation (cancel in-flight requests when first completes; reduces wasted work), and deadline propagation (drop requests exceeding deadline; prioritizes freshness). +**Mitigation strategies** include hedged requests (issue redundant reads, take first response; tradeoff: 2 $\times$ read amplification), backup requests (issue second request if first is slow; tradeoff: lower amplification, higher complexity), selective replica choice (route to least-loaded replica; tradeoff: requires load monitoring), request cancellation (cancel in-flight requests when first completes; reduces wasted work), and deadline propagation (drop requests exceeding deadline; prioritizes freshness). **Production example:** @@ -489,10 +489,10 @@ GFS makes several unconventional design choices: - Distributed metadata: The single master bottleneck is eliminated by sharding metadata across multiple servers - Smaller block sizes: 1 MB blocks improve space efficiency for smaller files -- Erasure coding[^fn-erasure-coding]: Reduces storage overhead from 3$\times$ replication to ~1.5$\times$ while maintaining durability +- Erasure coding[^fn-erasure-coding]: Reduces storage overhead from 3 $\times$ replication to ~1.5 $\times$ while maintaining durability - Reed-Solomon encoding [@reed1960polynomial]: Enables efficient reconstruction of failed blocks -[^fn-erasure-coding]: **Erasure Coding**: A data protection technique that breaks data into fragments, adds redundant parity fragments, and distributes them across storage nodes such that the original data can be reconstructed from any subset of fragments. A common configuration is RS(6,3): 6 data fragments plus 3 parity fragments, tolerating any 3 failures with only 1.5$\times$ storage overhead (versus 3$\times$ for triple replication). The trade-off is computational cost: reconstruction requires Galois field arithmetic that consumes CPU cycles. For ML training data that is read frequently but rarely reconstructed, erasure coding dramatically reduces storage costs at petabyte scale. +[^fn-erasure-coding]: **Erasure Coding**: A data protection technique that breaks data into fragments, adds redundant parity fragments, and distributes them across storage nodes such that the original data can be reconstructed from any subset of fragments. A common configuration is RS(6,3): 6 data fragments plus 3 parity fragments, tolerating any 3 failures with only 1.5 $\times$ storage overhead (versus 3 $\times$ for triple replication). The trade-off is computational cost: reconstruction requires Galois field arithmetic that consumes CPU cycles. For ML training data that is read frequently but rarely reconstructed, erasure coding dramatically reduces storage costs at petabyte scale. For ML workloads, Colossus delivers aggregate read bandwidth exceeding 1 TB/s across Google's infrastructure, sufficient to feed thousands of TPU chips simultaneously. @@ -502,7 +502,7 @@ HDFS[^fn-hdfs] [@shvachko2010hadoop], the open-source implementation inspired by - **NameNode**: Single master maintaining file system namespace and block locations - **DataNodes**: Workers storing actual data blocks -- **Block replication**: Default 3$\times$ replication for durability +- **Block replication**: Default 3 $\times$ replication for durability [^fn-hdfs]: **Hadoop Distributed File System (HDFS)**: Developed at Yahoo in 2006 as an open-source GFS clone, HDFS became the dominant distributed file system for big data workloads. Its design assumes commodity hardware failures are common and optimizes for batch processing with large files. HDFS stores files as sequences of 128 MB blocks (configurable), each replicated across three DataNodes. While effective for MapReduce workloads, HDFS's single-NameNode architecture limits metadata operations to roughly 10,000 per second, which becomes a bottleneck when ML training involves millions of small files or frequent checkpoint metadata updates. @@ -531,7 +531,7 @@ Lustre[^fn-lustre] [@schwan2003lustre], developed for high-performance computing **Lustre architecture** separates metadata from data more aggressively. Metadata Servers (MDS) handle file system namespace operations (create, open, stat), Object Storage Servers (OSS) store actual file data, and Object Storage Targets (OST) represent individual storage devices attached to OSSs. -Files are striped across multiple OSTs, enabling parallel access that aggregates bandwidth from many storage devices. A large file might be striped across 100 OSTs; reading the file in parallel achieves 100$\times$ the bandwidth of a single OST. +Files are striped across multiple OSTs, enabling parallel access that aggregates bandwidth from many storage devices. A large file might be striped across 100 OSTs; reading the file in parallel achieves 100 $\times$ the bandwidth of a single OST. **Lustre performance characteristics**: @@ -595,7 +595,7 @@ The request rate limit is significant for ML: a dataset organized as `s3://bucke **GCS performance** is similar, providing single object throughput of 100+ MB/s per connection, strong consistency (both GCS and S3 since December 2020 provide read-after-write consistency), and composite objects (multiple objects can be composed into one without re-upload). -Practical throughput depends heavily on access patterns. @tbl-object-storage-throughput reveals how sequential large-object reads can achieve 100$\times$ the throughput of small random lookups: +Practical throughput depends heavily on access patterns. @tbl-object-storage-throughput reveals how sequential large-object reads can achieve 100 $\times$ the throughput of small random lookups: | **Access Pattern** | **Typical Throughput** | **Optimization** | |:------------------------------|------------------------:|:--------------------------------------------| @@ -652,7 +652,7 @@ For large-scale training that processes entire datasets sequentially, TFRecord a Parquet[^fn-parquet], developed for the Hadoop ecosystem and inspired by Google's Dremel [@melnik2010dremel], uses columnar storage that stores all values of a column together rather than all columns of a row together. -[^fn-parquet]: **Apache Parquet**: An open-source columnar storage format created in 2013 by Twitter and Cloudera. The name references the geometric floor pattern where pieces fit together efficiently. Parquet stores data column-by-column rather than row-by-row, enabling efficient compression (similar values compress well) and selective column reads (skip columns not needed for analysis). For ML feature datasets with hundreds of columns where training uses only a subset, Parquet can reduce I/O by 10$\times$ compared to row-oriented formats. The trade-off is write amplification: updating a single value requires rewriting an entire row group (typically 128 MB). +[^fn-parquet]: **Apache Parquet**: An open-source columnar storage format created in 2013 by Twitter and Cloudera. The name references the geometric floor pattern where pieces fit together efficiently. Parquet stores data column-by-column rather than row-by-row, enabling efficient compression (similar values compress well) and selective column reads (skip columns not needed for analysis). For ML feature datasets with hundreds of columns where training uses only a subset, Parquet can reduce I/O by 10 $\times$ compared to row-oriented formats. The trade-off is write amplification: updating a single value requires rewriting an entire row group (typically 128 MB). **Columnar advantages for ML** include column pruning (read only the columns needed, such as skipping metadata and reading only pixels), compression efficiency (similar values in a column compress better than mixed values in a row), and predicate pushdown (filter data without reading irrelevant rows). @@ -824,7 +824,7 @@ Caching can dramatically improve data loading performance when datasets are acce **Distributed caching:** Services like Alluxio[^fn-alluxio] [@li2014tachyon] provide a distributed cache layer between compute and storage. Multiple nodes contribute memory to a shared cache, enabling datasets larger than single-node memory to be cached. -[^fn-alluxio]: **Alluxio (formerly Tachyon)**: An open-source virtual distributed storage system created at UC Berkeley AMPLab in 2013. Alluxio provides a unified namespace across multiple storage systems (S3, HDFS, Azure Blob) with memory-speed caching. For ML workloads, Alluxio can cache hot training data across cluster nodes, reducing repeated fetches from cold object storage. Meta uses a similar internal system to accelerate training data access, achieving 10$\times$ throughput improvement for frequently accessed datasets. +[^fn-alluxio]: **Alluxio (formerly Tachyon)**: An open-source virtual distributed storage system created at UC Berkeley AMPLab in 2013. Alluxio provides a unified namespace across multiple storage systems (S3, HDFS, Azure Blob) with memory-speed caching. For ML workloads, Alluxio can cache hot training data across cluster nodes, reducing repeated fetches from cold object storage. Meta uses a similar internal system to accelerate training data access, achieving 10 $\times$ throughput improvement for frequently accessed datasets. **Cache invalidation** for ML is straightforward: training datasets are immutable. The cache can use simple LRU eviction without concern for consistency. @@ -881,7 +881,7 @@ The cost of moving data between nodes dominates computation time for many ML wor | **RACK_LOCAL** | Data on another node in same rack | ~100 μs | 25 GB/s | | **ANY** | Data on any node in cluster | ~1 ms | 10 GB/s | -The 100$\times$ bandwidth difference between CPU_LOCAL (200 GB/s) and ANY (10 GB/s via network) makes locality-aware scheduling essential for bandwidth-intensive ML workloads. +The 100 $\times$ bandwidth difference between CPU_LOCAL (200 GB/s) and ANY (10 GB/s via network) makes locality-aware scheduling essential for bandwidth-intensive ML workloads. **Scheduling for Locality:** @@ -957,7 +957,7 @@ where $B_{gpu}$ is the effective data consumption rate of the accelerator, $T_{p ::: -This equation reveals that I/O requirements depend not just on GPU speed but on preprocessing complexity. Vision models with heavy augmentation (random crops, color jitter, mixup) may require 3-5$\times$ higher I/O bandwidth than the raw data consumption rate suggests, because preprocessing takes 3-5$\times$ longer than GPU computation. +This equation reveals that I/O requirements depend not just on GPU speed but on preprocessing complexity. Vision models with heavy augmentation (random crops, color jitter, mixup) may require 3-5 $\times$ higher I/O bandwidth than the raw data consumption rate suggests, because preprocessing takes 3-5 $\times$ longer than GPU computation. #### Sequential vs Random Access: The Performance Gap {#sec-storage-sequential-vs-random-access-performance-gap-51f6} @@ -970,15 +970,15 @@ Storage device performance varies dramatically with access pattern. The numbers | **NVMe SSD** | 3-7 GB/s | 500 MB/s - 1.5 GB/s | 15-25% | | **NVMe (4x RAID 0)** | 12-25 GB/s | 2-6 GB/s | 15-25% | -: **Storage Performance by Access Pattern**: HDDs suffer 200-300$\times$ degradation for random access due to mechanical seek time; SSDs suffer 4-7$\times$ degradation due to small transfer sizes not amortizing controller overhead. ML training pipelines must be designed for sequential access to achieve advertised throughput. {#tbl-io-access-patterns} +: **Storage Performance by Access Pattern**: HDDs suffer 200-300 $\times$ degradation for random access due to mechanical seek time; SSDs suffer 4-7 $\times$ degradation due to small transfer sizes not amortizing controller overhead. ML training pipelines must be designed for sequential access to achieve advertised throughput. {#tbl-io-access-patterns} The performance gap between sequential and random access has profound implications for ML data pipelines: **HDDs are unusable for random access ML workloads.** A 7200 RPM HDD delivers 150-200 MB/s sequential throughput, sufficient for a modest training pipeline. But random 4KB reads (the pattern when accessing individual small images from disk) yield only 0.5-1 MB/s due to 8-10ms seek times. A dataset of 1 million individual JPEG files on HDD would require over 10 days to read randomly, versus 2 hours sequentially. -**SSDs narrow but do not eliminate the gap.** NVMe SSDs achieve 3-7 GB/s sequential throughput, but random 4KB reads drop to 500 MB/s-1.5 GB/s. The 4-7$\times$ degradation occurs because small transfers do not amortize the fixed overhead per I/O operation (command processing, internal flash page reads). For ML, this means that storing images as individual files on NVMe still leaves significant performance on the table. +**SSDs narrow but do not eliminate the gap.** NVMe SSDs achieve 3-7 GB/s sequential throughput, but random 4KB reads drop to 500 MB/s-1.5 GB/s. The 4-7 $\times$ degradation occurs because small transfers do not amortize the fixed overhead per I/O operation (command processing, internal flash page reads). For ML, this means that storing images as individual files on NVMe still leaves significant performance on the table. -**Aggregated formats recover sequential performance.** Converting a dataset from individual files to TFRecord, WebDataset, or Parquet transforms random access into sequential streaming. A WebDataset TAR archive containing 10,000 images reads at full sequential bandwidth (7 GB/s on NVMe) rather than random access rates (1.5 GB/s). This 4-5$\times$ throughput improvement often makes the difference between I/O-bound and compute-bound training. +**Aggregated formats recover sequential performance.** Converting a dataset from individual files to TFRecord, WebDataset, or Parquet transforms random access into sequential streaming. A WebDataset TAR archive containing 10,000 images reads at full sequential bandwidth (7 GB/s on NVMe) rather than random access rates (1.5 GB/s). This 4-5 $\times$ throughput improvement often makes the difference between I/O-bound and compute-bound training. #### Worked Example: ImageNet Data Loading Bottleneck Analysis {#sec-storage-worked-example-imagenet-data-loading-bottleneck-analysis-8b6c} @@ -1006,7 +1006,7 @@ This 150 MB/s requirement seems modest. Even an HDD could sustain this rate for ImageNet stored as individual JPEG files (the common download format) creates a directory structure with 1000 class folders. Training with shuffling requires random access across the entire dataset: -- HDD random read: ~100 IOPS = 100 images/s (10$\times$ slower than target) +- HDD random read: ~100 IOPS = 100 images/s (10 $\times$ slower than target) - NVMe random read: ~300K IOPS = 300K images/s (sufficient, with overhead) #### Step 3: Account for Preprocessing Overhead {.unnumbered} @@ -1023,7 +1023,7 @@ $$T_{batch} = \frac{256}{1000} = 256 \text{ ms}$$ $$T_{preprocess} = 256 \times 5 \text{ ms} = 1280 \text{ ms (single-threaded)}$$ -Single-threaded preprocessing is 5$\times$ slower than GPU consumption. This requires 5 parallel workers to keep pace. +Single-threaded preprocessing is 5 $\times$ slower than GPU consumption. This requires 5 parallel workers to keep pace. #### Step 4: Calculate Effective I/O Requirement {.unnumbered} @@ -1033,14 +1033,14 @@ $$B_{effective} = 5 \times 150 \text{ MB/s} = 750 \text{ MB/s}$$ #### Step 5: Format Optimization Impact {.unnumbered} -| **Configuration** | **Read Pattern** | **Achievable** | **Bottleneck?** | -|:-----------------------------|:-----------------|---------------:|--------------------------:| -| **Individual JPEGs on HDD** | Random | 15 MB/s | Yes (50$\times$ too slow) | -| **Individual JPEGs on NVMe** | Random | 1.5 GB/s | No | -| **WebDataset on HDD** | Sequential | 150 MB/s | Yes (5$\times$ too slow) | -| **WebDataset on NVMe** | Sequential | 7 GB/s | No (9$\times$ headroom) | +| **Configuration** | **Read Pattern** | **Achievable** | **Bottleneck?** | +|:-----------------------------|:-----------------|---------------:|---------------------------:| +| **Individual JPEGs on HDD** | Random | 15 MB/s | Yes (50 $\times$ too slow) | +| **Individual JPEGs on NVMe** | Random | 1.5 GB/s | No | +| **WebDataset on HDD** | Sequential | 150 MB/s | Yes (5 $\times$ too slow) | +| **WebDataset on NVMe** | Sequential | 7 GB/s | No (9 $\times$ headroom) | -**Conclusion:** Individual JPEG storage on HDD creates a 50$\times$ I/O bottleneck. Converting to WebDataset and using NVMe provides 9$\times$ headroom, ensuring I/O never limits training throughput. +**Conclusion:** Individual JPEG storage on HDD creates a 50 $\times$ I/O bottleneck. Converting to WebDataset and using NVMe provides 9 $\times$ headroom, ensuring I/O never limits training throughput. ::: @@ -1105,13 +1105,13 @@ Data format selection impacts both storage efficiency and read throughput. @tbl- | **LMDB** | 4-6x | 10-20% | Yes (fast) | Repeated epochs | | **HDF5** | 2-4x | 5-15% | Yes (chunked) | Scientific data | -: **Data Format Performance Comparison**: Sequential formats (TFRecord, WebDataset) achieve 3-5$\times$ read speedup over individual files by eliminating per-file metadata overhead. LMDB provides fast random access through memory-mapped B-trees, ideal for workloads requiring non-sequential access. {#tbl-format-performance} +: **Data Format Performance Comparison**: Sequential formats (TFRecord, WebDataset) achieve 3-5 $\times$ read speedup over individual files by eliminating per-file metadata overhead. LMDB provides fast random access through memory-mapped B-trees, ideal for workloads requiring non-sequential access. {#tbl-format-performance} **TFRecord** excels for TensorFlow pipelines, providing native integration with `tf.data` and supporting transparent compression. The 5-10% storage overhead from record headers is offset by optional GZIP/ZSTD compression that typically reduces total size by 10-30%. **WebDataset** uses the ubiquitous TAR format, enabling direct streaming from HTTP/S3 without custom readers. Its minimal 1-2% overhead makes it storage-efficient, and the format's simplicity ensures long-term readability. -**Parquet** provides column pruning for tabular datasets where training uses a subset of features. A recommendation model training on 10 of 500 features reads only 2% of the data, yielding effective 50$\times$ speedup over row-oriented formats. +**Parquet** provides column pruning for tabular datasets where training uses a subset of features. A recommendation model training on 10 of 500 features reads only 2% of the data, yielding effective 50 $\times$ speedup over row-oriented formats. **LMDB** (Lightning Memory-Mapped Database) maintains a B-tree index enabling O(log n) random access while achieving near-sequential read speeds through memory mapping. This makes LMDB ideal for workloads that require random access (such as curriculum learning or hard example mining) without sacrificing throughput. @@ -1193,9 +1193,9 @@ Training large models requires days to months of continuous computation. During ### Checkpoint Architecture Fundamentals {#sec-storage-checkpoint-architecture-fundamentals-135d} -With the cost of lost computation established, we can examine precisely what state must be preserved to enable recovery. A checkpoint captures the complete state needed to resume training exactly where it left off. This includes model parameters (the current values of all trainable weights; for a `{python} gpt3_params_b` billion parameter model in FP16 [@brown2020gpt3], this is 350 GB of dense data), optimizer state (optimizers like Adam[^fn-adam-optimizer] [@kingma2015adam] maintain per-parameter statistics such as first and second moments, with Adam's state being 2$\times$ the size of model parameters, adding 700 GB for the `{python} gpt3_params_b`B parameter case), learning rate scheduler state (the current position in the learning rate schedule including step count and warmup progress), random number generator state (to ensure reproducibility, the RNG state for each worker must be saved), and data loader state (which samples have been seen in the current epoch, enabling resumption without repeating or skipping samples). +With the cost of lost computation established, we can examine precisely what state must be preserved to enable recovery. A checkpoint captures the complete state needed to resume training exactly where it left off. This includes model parameters (the current values of all trainable weights; for a `{python} gpt3_params_b` billion parameter model in FP16 [@brown2020gpt3], this is 350 GB of dense data), optimizer state (optimizers like Adam[^fn-adam-optimizer] [@kingma2015adam] maintain per-parameter statistics such as first and second moments, with Adam's state being 2 $\times$ the size of model parameters, adding 700 GB for the `{python} gpt3_params_b`B parameter case), learning rate scheduler state (the current position in the learning rate schedule including step count and warmup progress), random number generator state (to ensure reproducibility, the RNG state for each worker must be saved), and data loader state (which samples have been seen in the current epoch, enabling resumption without repeating or skipping samples). -[^fn-adam-optimizer]: **Adam (Adaptive Moment Estimation)**: The most widely used optimizer for deep learning, introduced in 2014. Adam maintains two exponential moving averages per parameter: the first moment (mean of gradients, for momentum) and second moment (mean of squared gradients, for adaptive learning rates). This 2$\times$ memory overhead becomes significant at scale: a `{python} gpt3_params_b`B parameter model requires 350 GB for weights and 700 GB for Adam state, totaling over 1 TB per checkpoint. Memory-efficient optimizers like Adafactor reduce this overhead by factorizing the second moment matrix, trading some convergence speed for 4-8$\times$ memory savings. +[^fn-adam-optimizer]: **Adam (Adaptive Moment Estimation)**: The most widely used optimizer for deep learning, introduced in 2014. Adam maintains two exponential moving averages per parameter: the first moment (mean of gradients, for momentum) and second moment (mean of squared gradients, for adaptive learning rates). This 2 $\times$ memory overhead becomes significant at scale: a `{python} gpt3_params_b`B parameter model requires 350 GB for weights and 700 GB for Adam state, totaling over 1 TB per checkpoint. Memory-efficient optimizers like Adafactor reduce this overhead by factorizing the second moment matrix, trading some convergence speed for 4-8 $\times$ memory savings. The total checkpoint size for a large model can reach several terabytes: @@ -1280,7 +1280,7 @@ When models are sharded across multiple devices using parallelism strategies (ex **Coordination challenge.** The storage system must ensure all devices write their shards simultaneously, capturing a consistent snapshot of distributed model state. If devices checkpoint at different training steps, recovery would load inconsistent weights, corrupting the model. Barrier synchronization across all devices precedes checkpoint writes, ensuring global consistency. -**Bandwidth challenge.** With 128 devices each writing simultaneously, the storage system must sustain 128$\times$ single-device write rates during checkpoint windows. For terabyte-scale checkpoints, this aggregate bandwidth requirement often exceeds what a single parallel file system can provide, necessitating tiered storage architectures that use local NVMe for immediate writes with background migration to durable distributed storage. +**Bandwidth challenge.** With 128 devices each writing simultaneously, the storage system must sustain 128 $\times$ single-device write rates during checkpoint windows. For terabyte-scale checkpoints, this aggregate bandwidth requirement often exceeds what a single parallel file system can provide, necessitating tiered storage architectures that use local NVMe for immediate writes with background migration to durable distributed storage. **Data parallel optimization.** When training uses data parallelism (where multiple devices hold identical model replicas), checkpointing can save just one replica's state. Since all replicas are identical, saving one replica reduces storage by the data parallelism degree. This optimization is transparent to the storage system but significantly reduces aggregate checkpoint volume. @@ -1309,7 +1309,7 @@ Storage fails after some but not all checkpoint shards are written. Without prot A storage server fails while training continues, potentially corrupting a checkpoint in progress. -*Solution: Replication.* With 3$\times$ replication, losing one storage node does not lose data. The storage system automatically redirects writes to surviving replicas. Checkpoint writes should require quorum acknowledgment (2 of 3 replicas) before confirming success. +*Solution: Replication.* With 3 $\times$ replication, losing one storage node does not lose data. The storage system automatically redirects writes to surviving replicas. Checkpoint writes should require quorum acknowledgment (2 of 3 replicas) before confirming success. **Checkpoint Corruption Detection:** @@ -1450,9 +1450,9 @@ For dense models where all parameters update every step, incremental checkpointi **Delta compression** compresses the difference between consecutive checkpoints rather than the absolute values. If parameters change by small amounts each step, the delta is highly compressible. Techniques include XOR encoding (store the XOR of current and previous parameter values), floating-point prediction (predict next value from previous and store residual), and lossy compression (accept small errors in checkpoint for large compression ratios). -Practical compression ratios depend on model type and training dynamics. Sparse embedding updates in recommendation systems achieve 20-100$\times$ compression; dense LLM training typically sees 10-20$\times$ reduction. +Practical compression ratios depend on model type and training dynamics. Sparse embedding updates in recommendation systems achieve 20-100 $\times$ compression; dense LLM training typically sees 10-20 $\times$ reduction. -@tbl-delta-checkpoint quantifies delta checkpointing effectiveness across model types: sparse embedding updates in recommendation systems achieve 20-100$\times$ compression, while dense LLM training typically sees 10-20$\times$ reduction. +@tbl-delta-checkpoint quantifies delta checkpointing effectiveness across model types: sparse embedding updates in recommendation systems achieve 20-100 $\times$ compression, while dense LLM training typically sees 10-20 $\times$ reduction. | **Model Type** | **Full Checkpoint** | **Delta Size** | **Compression Ratio** | |:--------------------------------------|--------------------:|---------------:|----------------------:| @@ -1460,7 +1460,7 @@ Practical compression ratios depend on model type and training dynamics. Sparse | **RecSys (sparse embedding updates)** | 10 TB | 100-500 GB | 20-100x | | **Vision (fine-tuning)** | 10 GB | 100 MB | 100x | -: **Delta Checkpoint Compression by Model Type**: Sparse RecSys embedding updates achieve 20-100$\times$ compression; dense LLM updates achieve 10-20x; fine-tuning frozen models achieves 100$\times$ by saving only adapter weights. {#tbl-delta-checkpoint} +: **Delta Checkpoint Compression by Model Type**: Sparse RecSys embedding updates achieve 20-100 $\times$ compression; dense LLM updates achieve 10-20x; fine-tuning frozen models achieves 100 $\times$ by saving only adapter weights. {#tbl-delta-checkpoint} ### Checkpoint Storage Architecture {#sec-storage-checkpoint-storage-architecture-0a1d} @@ -1496,7 +1496,7 @@ Checkpoint strategies vary significantly by model architecture. LLMs demand high **Vision Models** at typical scales (< 1 billion parameters) checkpoint easily. The entire checkpoint fits in a few GB, which can be written in under a second even to modest storage. The challenge is ensuring checkpoints are copied to durable storage before being deleted locally. -**Fine-tuning runs** of any model type can use delta checkpointing efficiently. Only the fine-tuned parameters (often < 1% of total for LoRA[^fn-lora]-style methods) need to be saved, reducing checkpoint size by 100$\times$ or more. +**Fine-tuning runs** of any model type can use delta checkpointing efficiently. Only the fine-tuned parameters (often < 1% of total for LoRA[^fn-lora]-style methods) need to be saved, reducing checkpoint size by 100 $\times$ or more. [^fn-lora]: **LoRA (Low-Rank Adaptation)**: A parameter-efficient fine-tuning technique introduced by Microsoft in 2021 that freezes the original model weights and trains small low-rank matrices that modify layer outputs. A typical LoRA configuration adds only 0.1-1% additional parameters while achieving performance comparable to full fine-tuning. For storage, LoRA dramatically reduces checkpoint sizes: a 7B parameter model fine-tuned with LoRA requires only 10-100 MB of adapter weights rather than 14 GB of full weights. This enables storing hundreds of fine-tuned variants without the storage cost of full model copies. @@ -1996,18 +1996,18 @@ Model artifacts range from megabytes (small classifiers) to terabytes (large fou **Large models (> 100 GB)**: Distributed storage with parallel download. Sharded across multiple files for parallel access. May require local NVMe for serving latency requirements. -Storage cost optimization strategies for model artifacts balance size reduction against access complexity. Compression achieves 2-5$\times$ size reduction at the cost of CPU overhead during loading; tiered storage moves old versions to cold storage but increases retrieval latency: +Storage cost optimization strategies for model artifacts balance size reduction against access complexity. Compression achieves 2-5 $\times$ size reduction at the cost of CPU overhead during loading; tiered storage moves old versions to cold storage but increases retrieval latency: -@tbl-artifact-storage summarizes these optimization strategies, from compression achieving 2-5$\times$ size reduction to tiered storage that moves old versions to cold storage at the cost of retrieval latency. +@tbl-artifact-storage summarizes these optimization strategies, from compression achieving 2-5 $\times$ size reduction to tiered storage that moves old versions to cold storage at the cost of retrieval latency. | **Strategy** | **Benefit** | **Tradeoff** | |:-------------------------|:------------------------------|:----------------------------------| -| **Compression** | 2-5$\times$ size reduction | CPU overhead on load | +| **Compression** | 2-5 $\times$ size reduction | CPU overhead on load | | **Deduplication** | Shared layers stored once | Complexity in artifact management | | **Tiered storage** | Cold storage for old versions | Retrieval latency | | **Differential storage** | Store only changed weights | Requires base model + diffs | -: **Model Artifact Storage Optimization**: Compression achieves 2-5$\times$ size reduction at the cost of CPU load time; deduplication stores shared layers once across model variants; tiered storage moves old versions to cold storage while maintaining retrieval capability. {#tbl-artifact-storage} +: **Model Artifact Storage Optimization**: Compression achieves 2-5 $\times$ size reduction at the cost of CPU load time; deduplication stores shared layers once across model variants; tiered storage moves old versions to cold storage while maintaining retrieval capability. {#tbl-artifact-storage} ### Model Registry Platforms {#sec-storage-model-registry-platforms-8746} @@ -2081,7 +2081,7 @@ storage_overhead = total_fragments / data_fragments storage_overhead_str = f"{storage_overhead:.2f}" ``` -**Erasure coding.** Colossus uses Reed-Solomon erasure coding [@reed1960polynomial] rather than simple replication. A typical configuration stores `{python} data_fragments` data fragments plus `{python} parity_fragments` parity fragments, achieving durability equivalent to 3$\times$ replication while using only `{python} storage_overhead_str` $\times$ storage. For petabyte-scale training data, this dramatically reduces storage costs. +**Erasure coding.** Colossus uses Reed-Solomon erasure coding [@reed1960polynomial] rather than simple replication. A typical configuration stores `{python} data_fragments` data fragments plus `{python} parity_fragments` parity fragments, achieving durability equivalent to 3 $\times$ replication while using only `{python} storage_overhead_str` $\times$ storage. For petabyte-scale training data, this dramatically reduces storage costs. **D (Disk) servers.** Colossus separates the storage layer into D servers that manage physical disks. This abstraction enables flexible placement of data across disk types (HDD for cold data, SSD for hot data) without changing the file system interface. @@ -2215,7 +2215,7 @@ Even organizations operating at the scales described above encounter recurring m **Fallacy:** ***Object storage latency is acceptable for training data.*** -Object stores like S3 and GCS offer seemingly infinite capacity at low cost, leading teams to treat them as drop-in replacements for file systems. However, first-byte latency of 50-200ms for object storage versus 1-10ms for distributed file systems means that small-batch access patterns incur unacceptable overhead. A training pipeline reading 1000 small files sequentially from S3 spends more time waiting for first bytes than transferring data. As @sec-storage-object-storage-scale-188b demonstrates, object storage remains the most cost-effective option for large datasets when pipelines are designed around its characteristics: large sequential reads (multi-MB), extensive prefetching, and local caching. Treating object storage as a transparent NFS replacement yields training throughput 3-5$\times$ lower than achievable with proper access pattern optimization. +Object stores like S3 and GCS offer seemingly infinite capacity at low cost, leading teams to treat them as drop-in replacements for file systems. However, first-byte latency of 50-200ms for object storage versus 1-10ms for distributed file systems means that small-batch access patterns incur unacceptable overhead. A training pipeline reading 1000 small files sequentially from S3 spends more time waiting for first bytes than transferring data. As @sec-storage-object-storage-scale-188b demonstrates, object storage remains the most cost-effective option for large datasets when pipelines are designed around its characteristics: large sequential reads (multi-MB), extensive prefetching, and local caching. Treating object storage as a transparent NFS replacement yields training throughput 3-5 $\times$ lower than achievable with proper access pattern optimization. **Pitfall:** ***Sizing checkpoint storage by capacity rather than bandwidth.*** @@ -2223,7 +2223,7 @@ Organizations provision checkpoint storage based on model size: "Our model is 50 **Fallacy:** ***Checkpoint storage is a solved problem.*** -Modern frameworks checkpoint transparently, creating the illusion that checkpointing "just works." This masks the coordination overhead that dominates checkpoint time at scale. With 1000 GPUs each writing 500MB, the aggregate checkpoint is 500GB. As @sec-storage-distributed-checkpointing-sharded-models-7369 explains, achieving this requires all ranks reaching the checkpoint barrier synchronously, coordinated storage access avoiding hotspots, verification that all shards completed successfully, and atomic update of the "latest checkpoint" pointer. The actual wall-clock time often exceeds theoretical write time by 2-3$\times$ due to stragglers, coordination, and verification. Organizations that benchmark single-node checkpointing (5 seconds) and linearly extrapolate to distributed settings (expecting 5 seconds) are consistently surprised when distributed checkpoint overhead yields 12-15 second wall-clock times. +Modern frameworks checkpoint transparently, creating the illusion that checkpointing "just works." This masks the coordination overhead that dominates checkpoint time at scale. With 1000 GPUs each writing 500MB, the aggregate checkpoint is 500GB. As @sec-storage-distributed-checkpointing-sharded-models-7369 explains, achieving this requires all ranks reaching the checkpoint barrier synchronously, coordinated storage access avoiding hotspots, verification that all shards completed successfully, and atomic update of the "latest checkpoint" pointer. The actual wall-clock time often exceeds theoretical write time by 2-3 $\times$ due to stragglers, coordination, and verification. Organizations that benchmark single-node checkpointing (5 seconds) and linearly extrapolate to distributed settings (expecting 5 seconds) are consistently surprised when distributed checkpoint overhead yields 12-15 second wall-clock times. **Pitfall:** ***Assuming training data locality exists.*** @@ -2269,15 +2269,15 @@ Training a recommendation model on features that include the label (using "user **Fallacy:** ***SSDs eliminate I/O bottlenecks for ML training.*** -The transition from HDDs to SSDs represents a transformative improvement in storage performance: 100$\times$ better random access latency, 10-30$\times$ higher IOPS, and 10-30$\times$ greater sequential bandwidth. This leads teams to assume that NVMe storage eliminates I/O as a bottleneck. As @sec-storage-io-optimization-ml-data-pipelines-b1ff quantifies, this assumption fails for three reasons. +The transition from HDDs to SSDs represents a transformative improvement in storage performance: 100 $\times$ better random access latency, 10-30 $\times$ higher IOPS, and 10-30 $\times$ greater sequential bandwidth. This leads teams to assume that NVMe storage eliminates I/O as a bottleneck. As @sec-storage-io-optimization-ml-data-pipelines-b1ff quantifies, this assumption fails for three reasons. -First, the GPU-storage bandwidth gap remains vast. An H100 GPU consumes data at `{python} h100_bw_tbs` TB/s from HBM. Even four NVMe drives in RAID 0 deliver only 25 GB/s, a 130$\times$ deficit. SSDs close the gap relative to HDDs but remain fundamentally slower than GPU memory bandwidth. +First, the GPU-storage bandwidth gap remains vast. An H100 GPU consumes data at `{python} h100_bw_tbs` TB/s from HBM. Even four NVMe drives in RAID 0 deliver only 25 GB/s, a 130 $\times$ deficit. SSDs close the gap relative to HDDs but remain fundamentally slower than GPU memory bandwidth. Second, preprocessing often dominates over I/O. Vision model training requires JPEG decoding (2-5 ms per image), random augmentation (1-2 ms), and normalization. At 1000 images/second, single-threaded preprocessing takes 7 seconds per second of training. The bottleneck shifts from storage to CPU, requiring 7+ parallel preprocessing workers. SSDs with their abundant bandwidth sit idle while CPUs struggle to decode images. -Third, access pattern matters more than raw speed. As @tbl-io-access-patterns demonstrates, NVMe SSDs achieve 7 GB/s sequential but only 1.5 GB/s for random 4KB reads. A dataset of 1 million individual JPEG files accessed randomly achieves only 20% of the drive's potential. Converting to WebDataset or TFRecord recovers the full 7 GB/s. The format optimization provides 5$\times$ speedup independent of storage medium. +Third, access pattern matters more than raw speed. As @tbl-io-access-patterns demonstrates, NVMe SSDs achieve 7 GB/s sequential but only 1.5 GB/s for random 4KB reads. A dataset of 1 million individual JPEG files accessed randomly achieves only 20% of the drive's potential. Converting to WebDataset or TFRecord recovers the full 7 GB/s. The format optimization provides 5 $\times$ speedup independent of storage medium. -The quantitative reality: a team upgrading from HDD to NVMe may see 3-5$\times$ improvement rather than the expected 30x, because preprocessing or access pattern inefficiencies were the true bottleneck. Effective I/O optimization requires profiling to identify the actual bottleneck, then addressing it systematically: storage upgrades help when storage is saturated, format optimization helps when access patterns are inefficient, and worker parallelism helps when preprocessing is the constraint. +The quantitative reality: a team upgrading from HDD to NVMe may see 3-5 $\times$ improvement rather than the expected 30x, because preprocessing or access pattern inefficiencies were the true bottleneck. Effective I/O optimization requires profiling to identify the actual bottleneck, then addressing it systematically: storage upgrades help when storage is saturated, format optimization helps when access patterns are inefficient, and worker parallelism helps when preprocessing is the constraint. ## Summary {#sec-storage-summary-7933-7933} diff --git a/book/quarto/contents/vol2/sustainable_ai/sustainable_ai.qmd b/book/quarto/contents/vol2/sustainable_ai/sustainable_ai.qmd index 8c4b4a5e2a..f374690db5 100644 --- a/book/quarto/contents/vol2/sustainable_ai/sustainable_ai.qmd +++ b/book/quarto/contents/vol2/sustainable_ai/sustainable_ai.qmd @@ -28,7 +28,7 @@ Power is not merely an operational expense but a hard physical constraint that l ::: {.callout-tip title="Learning Objectives"} -- Explain the sustainability paradox where AI compute growth (350,000$\times$ from 2012-2019) outpaces hardware efficiency gains, and analyze how Jevons Paradox causes efficiency improvements to increase total resource consumption +- Explain the sustainability paradox where AI compute growth (350,000 $\times$ from 2012-2019) outpaces hardware efficiency gains, and analyze how Jevons Paradox causes efficiency improvements to increase total resource consumption - Calculate Power Usage Effectiveness (PUE) and lifecycle carbon footprints across training (60-80%), inference (15-25%), and manufacturing (5-15%) phases, differentiating operational emissions from embodied carbon - Analyze geographic and temporal factors affecting carbon intensity, comparing emission differences across energy grids and applying these insights to workload scheduling decisions - Evaluate algorithmic optimization techniques (pruning, quantization, knowledge distillation) and edge deployment strategies in terms of accuracy-energy trade-offs and lifecycle sustainability impacts @@ -117,7 +117,7 @@ To appreciate the scale of the problem, consider the *carbon cost of training* a **The Systems Conclusion**: A single training run emits as much carbon as flying a Boeing 747 full of passengers across the Atlantic. **Optimization matters.** Moving this job to a hydro-powered region (0.02 kg/kWh) would reduce emissions by **20x** to just ~25 flights. ::: -AI systems consume resources at industrial scales that rival traditional heavy industries. Training a single large language model consumes thousands of megawatt-hours of electricity, equivalent to powering hundreds of households for months.[^fn-household-energy] Data centers that include AI workloads are projected to account for 8% of global power consumption by 2030, surpassing aviation at 2.1% and approaching cement production at 4% [@oecd2023blueprint].[^fn-industry-comparison] Computational demands increased 350,000$\times$ from 2012 to 2019 [@schwartz2020green], while hardware efficiency improved at a far slower rate, creating an unsustainable growth trajectory. +AI systems consume resources at industrial scales that rival traditional heavy industries. Training a single large language model consumes thousands of megawatt-hours of electricity, equivalent to powering hundreds of households for months.[^fn-household-energy] Data centers that include AI workloads are projected to account for 8% of global power consumption by 2030, surpassing aviation at 2.1% and approaching cement production at 4% [@oecd2023blueprint].[^fn-industry-comparison] Computational demands increased 350,000 $\times$ from 2012 to 2019 [@schwartz2020green], while hardware efficiency improved at a far slower rate, creating an unsustainable growth trajectory. [^fn-household-energy]: **Household Energy Comparison**: The average U.S. household consumes 10,500 kWh annually (about 875 kWh monthly). While OpenAI has not released official GPT-4 training energy consumption data, estimates suggest it may have required significantly more energy than GPT-3's verified 1,287 MWh. For context, GPT-3's training consumed electricity equivalent to 122 average U.S. households' annual consumption. @@ -127,7 +127,7 @@ Beyond direct energy consumption, AI systems drive environmental impact through [^fn-gpu-manufacturing]: **GPU Manufacturing Impact**: Producing a single high-end GPU like the NVIDIA H100 generates 300-500 kg of CO₂ before any computation occurs. Manufacturing requires 2,500+ liters of ultrapure water, 15+ rare earth elements, and energy-intensive processes reaching 1,000°C. TSMC's 4nm process is more energy-efficient per transistor but requires more complex manufacturing steps, increasing overall fab energy intensity compared to 7nm processes. -[^fn-ewaste-scale]: **E-Waste from Computing**: Global e-waste reached 53.6 million metric tons in 2019, with computing equipment contributing 15% [@forti2020ewaste]. AI hardware accelerates this trend: NVIDIA's GPU sales increased 200% from 2020-2023, with each high-end GPU weighing 2-4 lbs and containing toxic materials requiring specialized disposal. The rapid obsolescence cycle means AI hardware often becomes e-waste within 3-5 years. +[^fn-ewaste-scale]: **E-Waste from Computing**: Global e-waste reached 53.6 million metric tons in 2019, with computing equipment contributing 15% [@forti2020ewaste]. AI hardware accelerates this trend: NVIDIA's GPU sales increased 200% from 2020-2023, with each high-end GPU weighing 1-2 kg and containing toxic materials requiring specialized disposal. The rapid obsolescence cycle means AI hardware often becomes e-waste within 3-5 years. These environmental challenges require systematic understanding and coordinated response in technical, policy, and ethical dimensions to ensure AI development remains viable and responsible. @@ -137,17 +137,17 @@ Before examining measurement and mitigation strategies, we must understand the e ### Environmental Justice and Responsible Development {#sec-sustainable-ai-environmental-justice-responsible-development-3923} -The environmental impact of AI creates ethical responsibilities that extend beyond technical optimization. Environmental sustainability emerges as a critical component of trustworthy AI systems, extending the responsible AI principles examined in @sec-responsible-ai to include ecological stewardship. The computational resources required for AI development concentrate environmental costs on specific communities while distributing benefits unequally across global populations. Data centers consume between 1 and 3 percent of global electricity and 200 billion gallons of water annually for cooling, often in regions where energy grids rely on fossil fuels and water resources face stress from climate change. +The environmental impact of AI creates ethical responsibilities that extend beyond technical optimization. Environmental sustainability emerges as a critical component of trustworthy AI systems, extending the responsible AI principles examined in @sec-responsible-ai to include ecological stewardship. The computational resources required for AI development concentrate environmental costs on specific communities while distributing benefits unequally across global populations. Data centers consume between 1 and 3 percent of global electricity and 760 billion liters of water annually for cooling, often in regions where energy grids rely on fossil fuels and water resources face stress from climate change. This geographic concentration of environmental burden creates questions of environmental justice that align with broader responsible AI frameworks.[^fn-environmental-justice] Just as fairness considerations require examining who benefits from AI systems and who bears their risks, environmental responsibility demands understanding who pays the ecological costs of AI advancement. Communities hosting AI infrastructure bear disproportionate environmental burdens while having limited access to AI's economic benefits, exemplifying the need to extend ethical AI frameworks beyond algorithmic fairness to encompass environmental stewardship. -[^fn-environmental-justice]: **Environmental Justice**: Framework ensuring that environmental benefits and burdens are distributed fairly across all communities, regardless of race, color, or income. In AI context, this means data centers often locate in economically disadvantaged areas to access cheaper land and electricity, imposing environmental costs (pollution, water usage, heat) on communities with little political power to resist. Meanwhile, AI benefits (jobs, economic growth) concentrate in wealthy tech hubs. Examples: Microsoft's data center in rural Iowa uses 6 million gallons of water daily while local farmers face drought restrictions. +[^fn-environmental-justice]: **Environmental Justice**: Framework ensuring that environmental benefits and burdens are distributed fairly across all communities, regardless of race, color, or income. In AI context, this means data centers often locate in economically disadvantaged areas to access cheaper land and electricity, imposing environmental costs (pollution, water usage, heat) on communities with little political power to resist. Meanwhile, AI benefits (jobs, economic growth) concentrate in wealthy tech hubs. Examples: Microsoft's data center in rural Iowa uses 23 million liters of water daily while local farmers face drought restrictions. ### Exponential Growth vs Physical Constraints {#sec-sustainable-ai-exponential-growth-vs-physical-constraints-0f4e} -Exponential growth in computational demands challenges the long-term sustainability of AI training and deployment. Over the past decade, AI systems have scaled at an unprecedented rate, with compute requirements increasing 350,000$\times$ from 2012 to 2019 [@schwartz2020green].[^fn-ai-compute-growth] This trend continues as machine learning systems prioritize larger models with more parameters, larger training datasets, and higher computational complexity. Sustaining this trajectory poses sustainability challenges, as hardware efficiency gains fail to keep pace with rising AI workload demands. +Exponential growth in computational demands challenges the long-term sustainability of AI training and deployment. Over the past decade, AI systems have scaled at an unprecedented rate, with compute requirements increasing 350,000 $\times$ from 2012 to 2019 [@schwartz2020green].[^fn-ai-compute-growth] This trend continues as machine learning systems prioritize larger models with more parameters, larger training datasets, and higher computational complexity. Sustaining this trajectory poses sustainability challenges, as hardware efficiency gains fail to keep pace with rising AI workload demands. -[^fn-ai-compute-growth]: **AI Compute Explosion**: This 350,000$\times$ increase represents a doubling time of approximately 3.4 months, far exceeding Moore's Law's 2-year doubling cycle. For comparison, this is equivalent to going from the computational power of a smartphone to that of the world's largest supercomputer. The trend has only accelerated with large language models: GPT-4's training is estimated to have required 25$\times$ more compute than GPT-3, while models like PaLM-2 and Claude used even more computational resources. +[^fn-ai-compute-growth]: **AI Compute Explosion**: This 350,000 $\times$ increase represents a doubling time of approximately 3.4 months, far exceeding Moore's Law's 2-year doubling cycle. For comparison, this is equivalent to going from the computational power of a smartphone to that of the world's largest supercomputer. The trend has only accelerated with large language models: GPT-4's training is estimated to have required 25 $\times$ more compute than GPT-3, while models like PaLM-2 and Claude used even more computational resources. Historically, computational efficiency improved with advances in semiconductor technology. Moore's Law predicted that the number of transistors on a chip would double approximately every two years, leading to continuous improvements in processing power and energy efficiency.[^fn-sustainable-moores-law] However, Moore's Law is now reaching core physical limits, making further transistor scaling difficult and costly. Dennard scaling, which once ensured that smaller transistors would operate at lower power levels, has also ended, leading to stagnation in energy efficiency improvements per transistor.[^fn-dennard-scaling] @@ -243,14 +243,14 @@ Y,Date \node[fill=white,above=2pt of legend,anchor=center]{\small\bfseries Scenario}; \end{tikzpicture} ``` -: **Data Center Energy Projections**: Between 2010 and 2030, data center electricity usage is projected to increase sharply, particularly under worst-case scenarios where consumption could exceed 8,000 TWh by 2030 [@jones2018much]. The gap between best and worst scenarios—over 10$\times$ difference—demonstrates the critical importance of efficiency optimization at every layer of the systems stack. +: **Data Center Energy Projections**: Between 2010 and 2030, data center electricity usage is projected to increase sharply, particularly under worst-case scenarios where consumption could exceed 8,000 TWh by 2030 [@jones2018much]. The gap between best and worst scenarios—over 10 $\times$ difference—demonstrates the critical importance of efficiency optimization at every layer of the systems stack. ::: Training complex AI systems demands high levels of computing power, resulting in significant energy consumption. OpenAI's GPT-3 exemplifies this scale: training required 1,287 megawatt-hours of electricity, equivalent to powering 130 U.S. homes for an entire year [@maslej2023artificial].[^fn-sustainable-gpt3] This energy consumption represents the computational algorithms trained on large datasets that characterize modern large language models.[^fn-training-process] [^fn-training-process]: **Training Process**: Iterative optimization of model parameters through forward passes (computing predictions), loss calculation, and backward passes (gradient computation via backpropagation). Modern training runs millions of iterations across distributed hardware. GPT-4 training consumed an estimated 50+ GWh over several months, with gradient synchronization and checkpointing adding 15-30% communication overhead. -[^fn-sustainable-gpt3]: **GPT-3 Energy Consumption**: Training GPT-3 consumed approximately 1,287 MWh of electricity, equivalent to the annual energy consumption of 130 average American homes or the same amount of CO₂ as burning 500,000 pounds of coal. At average US electricity prices, this training run cost roughly $130,000 in electricity alone. GPT-4, with estimated 25$\times$ more compute, likely consumed over 30,000 MWh, enough to power a small city for a month. The energy per parameter ratio reveals hardware-software co-design inefficiencies: GPT-3's `{python} gpt3_params_b` billion parameters required 7.4 kWh per billion parameters, while optimized architectures can achieve sub-1 kWh ratios through mixed precision and sparsity techniques. +[^fn-sustainable-gpt3]: **GPT-3 Energy Consumption**: Training GPT-3 consumed approximately 1,287 MWh of electricity, equivalent to the annual energy consumption of 130 average American homes or the same amount of CO₂ as burning 227,000 kg of coal. At average US electricity prices, this training run cost roughly $130,000 in electricity alone. GPT-4, with estimated 25 $\times$ more compute, likely consumed over 30,000 MWh, enough to power a small city for a month. The energy per parameter ratio reveals hardware-software co-design inefficiencies: GPT-3's `{python} gpt3_params_b` billion parameters required 7.4 kWh per billion parameters, while optimized architectures can achieve sub-1 kWh ratios through mixed precision and sparsity techniques. This scale of energy consumption highlights the urgent need for efficiency improvements in AI systems. Generative AI models have gained increasing popularity in recent years, leading to more models being trained with growing parameter counts. @@ -260,7 +260,7 @@ Research shows that increasing model size, dataset size, and compute used for tr Beyond electricity consumption, the sustainability challenges of AI extend to hardware resource demands and the energy efficiency limitations of current architectures. Different processor types affect environmental impact through their energy characteristics. Central Processing Units consume approximately 100 picojoules per multiply-accumulate operation, Graphics Processing Units achieve 10 pJ/MAC, while specialized Tensor Processing Units reach 1 pJ/MAC, and specialized accelerators approach 0.1 pJ/MAC.[^fn-energy-metrics] These hardware platforms require rare earth metals and complex manufacturing processes with embodied carbon. -[^fn-energy-metrics]: **Energy Metrics (pJ/MAC)**: Picojoules per multiply-accumulate operation, the standard measure of computational energy efficiency. CPUs typically consume 20-100 pJ/MAC, GPUs achieve 0.5-2 pJ/MAC, and specialized TPUs reach 0.1-0.5 pJ/MAC. The human brain operates at ~1 fJ/op (1000$\times$ more efficient), establishing the theoretical ceiling for neuromorphic computing research. +[^fn-energy-metrics]: **Energy Metrics (pJ/MAC)**: Picojoules per multiply-accumulate operation, the standard measure of computational energy efficiency. CPUs typically consume 20-100 pJ/MAC, GPUs achieve 0.5-2 pJ/MAC, and specialized TPUs reach 0.1-0.5 pJ/MAC. The human brain operates at ~1 fJ/op (1000 $\times$ more efficient), establishing the theoretical ceiling for neuromorphic computing research. The production of AI chips is energy-intensive, involving multiple fabrication steps that contribute significantly to Scope 3 emissions in the overall AI system lifecycle. As model sizes continue to grow, the demand for AI hardware increases, exacerbating the environmental impact of semiconductor production and disposal. @@ -280,7 +280,7 @@ Physical efficiency in information processing stems from three key principles th 3. **Sparsity and Sparse Interconnects**: In modern GPUs, the majority of energy is spent on data movement and global synchronization. High-efficiency systems utilize sparse representations where only 1-2% of parameters are active for any given task, drastically reducing the bandwidth and switching energy required. -[^fn-event-driven]: **Event-Driven Computing**: A paradigm where computation is triggered by changes in input (events) rather than a continuous clock signal. This approach is the foundation of *Neuromorphic* hardware, which can achieve 100-1000$\times$ energy reductions for specific temporal tasks by remaining idle when data is static. +[^fn-event-driven]: **Event-Driven Computing**: A paradigm where computation is triggered by changes in input (events) rather than a continuous clock signal. This approach is the foundation of *Neuromorphic* hardware, which can achieve 100-1000 $\times$ energy reductions for specific temporal tasks by remaining idle when data is static. These insights point toward promising research directions for sustainable AI. Architectures that implement **Spiking Neural Networks (SNNs)** or sparse activation patterns can achieve significant energy reductions by mimicking these sparse communication models [@prakash2023tinyml].[^fn-spiking-networks] Similarly, local learning algorithms and self-supervised approaches offer pathways toward more sample-efficient and energy-conscious systems. @@ -406,9 +406,9 @@ Carbon footprint analysis must examine both lifecycle phases and emission scopes Effective carbon footprint measurement requires systematic analysis across three distinct phases that collectively determine environmental impact: -The training phase (60-80% of emissions) represents the most carbon-intensive period involving parallel computation for mathematical optimization processes[^fn-optimization-process]. As demonstrated by the GPT-3 case study, large language model training runs exemplify this energy intensity. Geographic placement affects emissions: training in Quebec (hydro-powered, 0.01 kg CO₂/kWh) versus West Virginia (coal-powered, 0.75 kg CO₂/kWh) creates a 75$\times$ difference in carbon intensity[^fn-carbon-intensity]. +The training phase (60-80% of emissions) represents the most carbon-intensive period involving parallel computation for mathematical optimization processes[^fn-optimization-process]. As demonstrated by the GPT-3 case study, large language model training runs exemplify this energy intensity. Geographic placement affects emissions: training in Quebec (hydro-powered, 0.01 kg CO₂/kWh) versus West Virginia (coal-powered, 0.75 kg CO₂/kWh) creates a 75 $\times$ difference in carbon intensity[^fn-carbon-intensity]. -[^fn-optimization-process]: **Optimization Process**: Mathematical procedures finding optimal parameters through gradient descent variants (SGD, Adam, AdaFactor) that iteratively adjust weights to minimize loss functions. Adam, introduced in 2014, maintains per-parameter learning rates and momentum estimates, requiring 3$\times$ memory overhead but enabling 2-10$\times$ faster convergence compared to vanilla SGD on modern transformer architectures. +[^fn-optimization-process]: **Optimization Process**: Mathematical procedures finding optimal parameters through gradient descent variants (SGD, Adam, AdaFactor) that iteratively adjust weights to minimize loss functions. Adam, introduced in 2014, maintains per-parameter learning rates and momentum estimates, requiring 3 $\times$ memory overhead but enabling 2-10 $\times$ faster convergence compared to vanilla SGD on modern transformer architectures. [^fn-carbon-intensity]: **Carbon Intensity**: Measure of CO₂ emissions per unit of electricity consumed, typically expressed as kg CO₂/kWh. Varies dramatically by energy source: coal (~0.82 kg CO₂/kWh), natural gas (~0.36), wind (~0.01), nuclear (~0.006), hydro (~0.024). Grid carbon intensity changes by location (Iceland: 99% renewable, Poland: 77% coal) and time of day (solar peaks at noon, wind varies). This enables carbon-aware computing: scheduling AI workloads when/where electricity is cleanest. @@ -416,7 +416,7 @@ The inference phase, which accounts for 15 to 25 percent of emissions, generates The manufacturing phase, which accounts for 5 to 15 percent of emissions, contributes embodied carbon from hardware production, including semiconductor fabrication, rare earth mining, and supply chain logistics.[^fn-embodied-carbon] Often overlooked, this phase represents irreducible baseline emissions independent of operational efficiency. -[^fn-embodied-carbon]: **Embodied Carbon**: Carbon emissions from manufacturing, transportation, and disposal phases of a product, distinct from operational emissions during use. For AI hardware, embodied carbon includes mining rare earth elements, semiconductor fabrication, packaging, and shipping. A single NVIDIA H100 GPU embodies 300-500 kg CO2 before first use, equivalent to 1,000-1,600 miles of driving. For comparison, the GPU's `{python} h100_tdp_w`W power consumption generates 300 kg CO2 annually (assuming average U.S. grid), meaning manufacturing emissions equal 1-2 years of operation. Research indicates that manufacturing emissions alone can account for up to 30% of an AI system's total carbon footprint, with this number potentially growing as data centers improve their reliance on renewable energy sources. +[^fn-embodied-carbon]: **Embodied Carbon**: Carbon emissions from manufacturing, transportation, and disposal phases of a product, distinct from operational emissions during use. For AI hardware, embodied carbon includes mining rare earth elements, semiconductor fabrication, packaging, and shipping. A single NVIDIA H100 GPU embodies 300-500 kg CO2 before first use, equivalent to 1,600-2,600 km of driving. For comparison, the GPU's `{python} h100_tdp_w`W power consumption generates 300 kg CO2 annually (assuming average U.S. grid), meaning manufacturing emissions equal 1-2 years of operation. Research indicates that manufacturing emissions alone can account for up to 30% of an AI system's total carbon footprint, with this number potentially growing as data centers improve their reliance on renewable energy sources. #### Geographic and Temporal Optimization {#sec-sustainable-ai-geographic-temporal-optimization-492c} @@ -540,13 +540,13 @@ Geographic optimization can dramatically reduce carbon emissions by choosing tra \node[right] at (9,-2.25) {High Carbon}; % Annotation - \node[align=center] at (4, 2) {\textbf{Geographic Optimization}\\Training in Quebec vs Poland reduces emissions by 40$\times$.}; + \node[align=center] at (4, 2) {\textbf{Geographic Optimization}\\Training in Quebec vs Poland reduces emissions by 40 $\times$.}; \draw[->, thick] (4, 1.5) -- (Quebec); \draw[->, thick] (4, 1.5) -- (Poland); \end{tikzpicture} ``` -**Geographic Carbon Intensity**. The carbon footprint of a training job depends critically on *where* it runs. Regions with hydro or nuclear power (e.g., Quebec, France) have carbon intensities 10-50$\times$ lower than regions reliant on coal (e.g., Poland, West Virginia). Carbon-aware scheduling exploits this variance by moving non-urgent jobs to cleaner grids. +**Geographic Carbon Intensity**. The carbon footprint of a training job depends critically on *where* it runs. Regions with hydro or nuclear power (e.g., Quebec, France) have carbon intensities 10-50 $\times$ lower than regions reliant on coal (e.g., Poland, West Virginia). Carbon-aware scheduling exploits this variance by moving non-urgent jobs to cleaner grids. ::: ### Systematic Energy Metrics {#sec-sustainable-ai-systematic-energy-metrics-5779-5779} @@ -574,7 +574,7 @@ Hardware architecture determines energy efficiency across orders of magnitude, s These differences reflect not just circuit-level efficiency but also architectural choices affecting utilization. CPUs execute diverse instruction mixes with low average utilization of arithmetic units. GPUs achieve higher utilization through massive parallelism. TPUs and ASICs maximize utilization through specialized datapaths optimized for specific operation types. -Precision significantly affects energy per operation. INT8 integer arithmetic consumes approximately one-sixteenth the energy of FP32 floating-point at the same frequency and voltage. This combines reduced datapath capacitance of 4$\times$ from bit width with lower voltage requirements of 2$\times$ from larger noise margins and simpler control logic of 2$\times$ from reduced complexity. +Precision significantly affects energy per operation. INT8 integer arithmetic consumes approximately one-sixteenth the energy of FP32 floating-point at the same frequency and voltage. This combines reduced datapath capacitance of 4 $\times$ from bit width with lower voltage requirements of 2 $\times$ from larger noise margins and simpler control logic of 2 $\times$ from reduced complexity. #### Energy Per Byte {#sec-sustainable-ai-energy-per-byte-5d87-5d87} @@ -1042,21 +1042,21 @@ print(f"Embodied fraction: {result['embodied_fraction']:.1%}") AI systems represent among the most energy-intensive computational workloads, involving dense operations with consumption patterns that extend across training, inference, data storage, and communication infrastructure.[^fn-dense-operations] Understanding these patterns reveals where optimization efforts can achieve environmental impact reduction. Energy consumption scales non-linearly with model complexity, creating opportunities for efficiency improvements through targeted architectural and operational optimizations. -[^fn-dense-operations]: **Dense Operations**: Computational patterns where most elements participate in calculations, including matrix multiplications, convolutions, and attention. Dense matrix-matrix multiply achieves 2N³ FLOPs for N$\times$N matrices, enabling high arithmetic intensity (>100 FLOP/byte) that maximizes GPU utilization. Transformers are particularly dense, with attention requiring O(N²) operations per token. +[^fn-dense-operations]: **Dense Operations**: Computational patterns where most elements participate in calculations, including matrix multiplications, convolutions, and attention. Dense matrix-matrix multiply achieves 2N³ FLOPs for N $\times$ N matrices, enabling high arithmetic intensity (>100 FLOP/byte) that maximizes GPU utilization. Transformers are particularly dense, with attention requiring O(N²) operations per token. #### Data Center Energy and AI Workloads {#sec-sustainable-ai-data-center-energy-ai-workloads-b1a8} Data centers serve as the primary energy consumers for AI systems, with power demands that reveal both the scale of the challenge and specific optimization opportunities. -Data center energy efficiency varies significantly across facilities. Power Usage Effectiveness ranges from 1.1 in Google's most efficient facilities to 2.5 in typical enterprise data centers, effectively doubling energy consumption through infrastructure overhead. Geographic location impacts carbon intensity. Training the same model in Quebec with hydro power versus West Virginia with coal power differs by 10$\times$ in carbon emissions per kilowatt-hour. Without access to renewable energy, these facilities rely heavily on nonrenewable sources such as coal and natural gas, contributing to global carbon emissions. Current estimates suggest that data centers produce up to 2 percent of total global CO₂ emissions, a figure that approaches the airline industry's footprint [@liu2020energy].[^fn-datacenter-emissions] The energy burden of AI is expected to grow exponentially due to three factors: increasing data center capacity, rising AI training workloads, and increasing inference demands [@patterson2022carbon]. Without intervention, these trends risk making AI's environmental footprint unsustainably large [@thompson2023compute]. +Data center energy efficiency varies significantly across facilities. Power Usage Effectiveness ranges from 1.1 in Google's most efficient facilities to 2.5 in typical enterprise data centers, effectively doubling energy consumption through infrastructure overhead. Geographic location impacts carbon intensity. Training the same model in Quebec with hydro power versus West Virginia with coal power differs by 10 $\times$ in carbon emissions per kilowatt-hour. Without access to renewable energy, these facilities rely heavily on nonrenewable sources such as coal and natural gas, contributing to global carbon emissions. Current estimates suggest that data centers produce up to 2 percent of total global CO₂ emissions, a figure that approaches the airline industry's footprint [@liu2020energy].[^fn-datacenter-emissions] The energy burden of AI is expected to grow exponentially due to three factors: increasing data center capacity, rising AI training workloads, and increasing inference demands [@patterson2022carbon]. Without intervention, these trends risk making AI's environmental footprint unsustainably large [@thompson2023compute]. [^fn-datacenter-emissions]: **Data Center Climate Impact**: Data centers consume approximately 1% of global electricity and produce 0.3% of global carbon emissions directly. However, when including embodied carbon from hardware manufacturing, the figure rises to 2%. For perspective, this equals the annual emissions of Argentina (1.8% of global total) and exceeds the aviation industry's 2.1%. The largest hyperscale data centers consume over 100 MW continuously, equivalent to powering 80,000 homes. #### Energy Demands in Data Centers {#sec-sustainable-ai-energy-demands-data-centers-b7ba} -AI workloads are among the most compute-intensive operations in modern data centers. Companies such as Meta operate hyperscale data centers spanning multiple football fields in size, housing hundreds of thousands of AI-optimized servers.[^fn-hyperscale-size] The training of large language models such as GPT-4 required over 25,000 Nvidia A100 GPUs running continuously for 90 to 100 days [@semianalysisGPT4], consuming thousands of megawatt-hours of electricity. These facilities rely on high-performance AI accelerators like NVIDIA DGX H100 units, each of which can draw up to 10.2 kW at peak power [@nvidiadgxH100]. The energy efficiency gap becomes clear when comparing hardware generations. H100 GPUs achieve approximately 2.5 to 3$\times$ better performance per watt than A100s for AI training workloads, while mixed-precision training can reduce energy consumption by 15 to 30 percent depending on model architecture and hardware through reduced computational precision with minimal accuracy impact [@gholami2021survey]. +AI workloads are among the most compute-intensive operations in modern data centers. Companies such as Meta operate hyperscale data centers spanning multiple football fields in size, housing hundreds of thousands of AI-optimized servers.[^fn-hyperscale-size] The training of large language models such as GPT-4 required over 25,000 Nvidia A100 GPUs running continuously for 90 to 100 days [@semianalysisGPT4], consuming thousands of megawatt-hours of electricity. These facilities rely on high-performance AI accelerators like NVIDIA DGX H100 units, each of which can draw up to 10.2 kW at peak power [@nvidiadgxH100]. The energy efficiency gap becomes clear when comparing hardware generations. H100 GPUs achieve approximately 2.5 to 3 $\times$ better performance per watt than A100s for AI training workloads, while mixed-precision training can reduce energy consumption by 15 to 30 percent depending on model architecture and hardware through reduced computational precision with minimal accuracy impact [@gholami2021survey]. -[^fn-hyperscale-size]: **Hyperscale Data Center Scale**: Meta's Prineville data center spans 2.5 million square feet (57 football fields) and houses 150,000+ servers. Microsoft's largest Azure data center in Iowa covers 700 acres with power capacity of 300 MW. Google operates 21 hyperscale facilities globally, consuming 12.2 TWh annually—more electricity than entire countries like Lithuania or Sri Lanka. +[^fn-hyperscale-size]: **Hyperscale Data Center Scale**: Meta's Prineville data center spans 230,000 m² (57 football fields) and houses 150,000+ servers. Microsoft's largest Azure data center in Iowa covers 283 hectares with power capacity of 300 MW. Google operates 21 hyperscale facilities globally, consuming 12.2 TWh annually—more electricity than entire countries like Lithuania or Sri Lanka. This dramatic energy consumption reflects AI's rapid adoption across industries. @fig-ai-data-center-demand projects that AI workload energy demand will increase total data center energy use significantly after 2024, with the AI segment growing from roughly 10% to over 30% of total power consumption by 2030. While efficiency gains have offset rising power needs in the past, these gains are decelerating, amplifying AI's environmental impact. @@ -1143,15 +1143,15 @@ ax.set_ylim(0, 1100) plt.show() ``` -Beyond computational demands, cooling represents another major factor in AI's energy footprint. Large-scale AI training and inference workloads generate massive amounts of heat, necessitating advanced cooling solutions to prevent hardware failures. Companies have begun adopting alternative cooling methods to reduce this demand. For example, Microsoft's data center in Ireland uses a nearby fjord, consuming over half a million gallons of seawater daily to dissipate heat. However, as AI models scale in complexity, cooling demands continue to grow, making sustainable AI infrastructure design a pressing challenge. +Beyond computational demands, cooling represents another major factor in AI's energy footprint. Large-scale AI training and inference workloads generate massive amounts of heat, necessitating advanced cooling solutions to prevent hardware failures. Companies have begun adopting alternative cooling methods to reduce this demand. For example, Microsoft's data center in Ireland uses a nearby fjord, consuming over 2 million liters of seawater daily to dissipate heat. However, as AI models scale in complexity, cooling demands continue to grow, making sustainable AI infrastructure design a pressing challenge. ### Distributed Systems Energy Optimization {#sec-sustainable-ai-distributed-systems-energy-optimization-5e83} -Large-scale AI training inherently requires distributed systems coordination, creating additional energy overhead that compounds computational demands. The parallelism strategies examined in @sec-distributed-training-systems introduce network communication costs that can account for 20-40% of total energy consumption in large clusters.[^fn-training-paradigms] This coordination across thousands of GPUs requires constant synchronization of computational updates and model parameters[^fn-distributed-training], generating data movement between nodes. This communication overhead scales poorly: doubling cluster size can increase networking energy consumption by 4$\times$ due to all-to-all communication patterns in gradient aggregation. +Large-scale AI training inherently requires distributed systems coordination, creating additional energy overhead that compounds computational demands. The parallelism strategies examined in @sec-distributed-training-systems introduce network communication costs that can account for 20-40% of total energy consumption in large clusters.[^fn-training-paradigms] This coordination across thousands of GPUs requires constant synchronization of computational updates and model parameters[^fn-distributed-training], generating data movement between nodes. This communication overhead scales poorly: doubling cluster size can increase networking energy consumption by 4 $\times$ due to all-to-all communication patterns in gradient aggregation. [^fn-training-paradigms]: The distributed training paradigms established in @sec-distributed-training-systems (data, model, and pipeline parallelism) each introduce distinct communication patterns with associated energy overhead. GPT-3 combined all three across thousands of GPUs, with parallelism strategy selection significantly impacting training efficiency and energy consumption. -[^fn-distributed-training]: **Distributed Training**: Parallelizing model training across multiple nodes through coordinated gradient synchronization and parameter updates. Ring-allreduce achieves near-linear scaling to hundreds of GPUs; hierarchical approaches scale to thousands. Communication overhead consumes 10-40% of training time, motivating gradient compression (1-2$\times$ speedup) and asynchronous methods (looser consistency for better throughput). +[^fn-distributed-training]: **Distributed Training**: Parallelizing model training across multiple nodes through coordinated gradient synchronization and parameter updates. Ring-allreduce achieves near-linear scaling to hundreds of GPUs; hierarchical approaches scale to thousands. Communication overhead consumes 10-40% of training time, motivating gradient compression (1-2 $\times$ speedup) and asynchronous methods (looser consistency for better throughput). Addressing these communication overheads, cluster-wide energy optimization requires coordinated resource management that extends beyond individual server efficiency. Dynamic workload placement can achieve 15-25% energy savings by consolidating training jobs onto fewer nodes during low-demand periods, allowing unused hardware to enter low-power states. Similarly, intelligent scheduling that coordinates training across multiple data centers can leverage time-zone differences and regional renewable energy availability, reducing carbon intensity by 30-50% through temporal load balancing. @@ -1161,9 +1161,9 @@ Infrastructure sharing presents efficiency opportunities often overlooked in sus The environmental impact of AI workloads has emerged as a concern, with carbon emissions approaching levels comparable to established carbon-intensive sectors. Research demonstrates that training a single large AI model generates carbon emissions equivalent to multiple passenger vehicles over their complete lifecycle [@strubell2019energy]. To contextualize AI's environmental footprint, @fig-carbonfootprint compares the carbon emissions of large-scale machine learning tasks to transcontinental flights, illustrating the energy demands of training and inference workloads. It shows a comparison from lowest to highest carbon footprints, starting with a roundtrip flight between NY and SF, human life average per year, American life average per year, US car including fuel over a lifetime, and a Transformer model with neural architecture search[^fn-transformer-nas], which has the highest footprint. These comparisons underscore the need for more sustainable AI practices to mitigate the industry's carbon impact. -[^fn-transformer-nas]: **Transformer + NAS Environmental Impact**: This 626,000 lbs CO₂ figure represents training one Transformer model while searching for optimal architecture. Includes evaluating 12,800 different model configurations over multiple days. For comparison, this equals the carbon footprint of 312 economy round-trip flights from NYC to London, or the annual emissions of 140 average Americans. Modern efficient NAS techniques have reduced this cost by 1000$\times$. +[^fn-transformer-nas]: **Transformer + NAS Environmental Impact**: This 284,000 kg (626,000 lbs) CO₂ figure represents training one Transformer model while searching for optimal architecture. Includes evaluating 12,800 different model configurations over multiple days. For comparison, this equals the carbon footprint of 312 economy round-trip flights from NYC to London, or the annual emissions of 140 average Americans. Modern efficient NAS techniques have reduced this cost by 1000 $\times$. -::: {#fig-carbonfootprint fig-env="figure" fig-pos="htb" fig-cap="**Carbon Footprint Benchmarks**: Training large AI models generates carbon emissions, comparable to everyday activities and long-distance travel, emphasizing the environmental impact of increasingly complex machine learning workloads. The comparison to roundtrip flights, average human lifespans, and vehicle lifetimes contextualizes the energy demands of training a transformer model with neural architecture search as high. Source: @strubell2019energy." fig-alt="Horizontal bar chart of CO2 emissions in pounds. From lowest to highest: NY-SF flight at 1,984, human life at 11,023, American life at 36,156, US car lifetime at 126,000, Transformer with NAS at 626,155 pounds."} +::: {#fig-carbonfootprint fig-env="figure" fig-pos="htb" fig-cap="**Carbon Footprint Benchmarks**: Training large AI models generates carbon emissions, comparable to everyday activities and long-distance travel, emphasizing the environmental impact of increasingly complex machine learning workloads. The comparison to roundtrip flights, average human lifespans, and vehicle lifetimes contextualizes the energy demands of training a transformer model with neural architecture search as high. Source: @strubell2019energy." fig-alt="Horizontal bar chart of CO2 emissions in lbs (source units). From lowest to highest: NY-SF flight at 1,984, human life at 11,023, American life at 36,156, US car lifetime at 126,000, Transformer with NAS at 626,155."} ```{.tikz} \begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}] \begin{axis}[title = {Common carbon footprint benchmarks}, @@ -1339,11 +1339,11 @@ Comprehensive carbon footprint assessment integrates the Three-Phase Lifecycle A Scope 1 emissions, which account for 5 to 15 percent of the total, originate from on-site power generation including backup diesel generators, facility cooling systems, and owned power plants. While many AI data centers primarily use grid electricity, those with fossil-fuel backup systems or owned generation contribute directly to emissions. -Scope 2 emissions, which account for 60 to 75 percent of the total, represent indirect emissions from electricity purchased to power AI infrastructure. This dominant operational emission category varies dramatically by geographic location and grid energy mix. As established in our geographic optimization discussion, training location can create up to 75$\times$ differences in carbon intensity. +Scope 2 emissions, which account for 60 to 75 percent of the total, represent indirect emissions from electricity purchased to power AI infrastructure. This dominant operational emission category varies dramatically by geographic location and grid energy mix. As established in our geographic optimization discussion, training location can create up to 75 $\times$ differences in carbon intensity. Scope 3 emissions, which account for 15 to 25 percent of the total, constitute the most complex category, encompassing hardware manufacturing, transportation, and disposal. Semiconductor manufacturing is carbon-intensive.[^fn-euv-lithography] Producing a single high-performance AI accelerator generates emissions equivalent to several years of operational energy use. Often overlooked, this category represents irreducible baseline emissions independent of operational efficiency. -[^fn-euv-lithography]: **EUV Lithography**: Extreme ultraviolet light (13.5nm wavelength) used to print features smaller than 7nm on silicon chips. Each EUV machine costs $200+ million, weighs 180 tons, requires 1 MW of continuous power (enough for 800 homes), and uses 30,000 liters of ultrapure water daily. ASML is the sole global supplier. EUV enables modern AI chips but consumes 10$\times$ more energy than older deep-UV lithography systems. +[^fn-euv-lithography]: **EUV Lithography**: Extreme ultraviolet light (13.5nm wavelength) used to print features smaller than 7nm on silicon chips. Each EUV machine costs $200+ million, weighs 180 tons, requires 1 MW of continuous power (enough for 800 homes), and uses 30,000 liters of ultrapure water daily. ASML is the sole global supplier. EUV enables modern AI chips but consumes 10 $\times$ more energy than older deep-UV lithography systems. Beyond manufacturing, Scope 3 emissions include the downstream impact of AI once deployed. AI services such as search engines, social media platforms, and cloud-based recommendation systems operate at enormous scale, requiring continuous inference across millions or even billions of user interactions. The cumulative electricity demand of inference workloads can ultimately surpass the energy used for training, further amplifying AI's carbon impact. End-user devices, including smartphones, IoT devices, and edge computing[^fn-edge-computing] platforms, also contribute to Scope 3 emissions, as their AI-allowed functionality depends on sustained computation. Companies such as Meta and Google report that Scope 3 emissions from AI-powered services make up the largest share of their total environmental footprint, due to the sheer scale at which AI operates. @@ -1520,7 +1520,7 @@ The distinction between "Prefill" and "Decode" established in @sec-inference-sca - **Prefill (Compute-Bound)**: High arithmetic intensity allows the GPU to perform thousands of operations for every byte read from memory, achieving near-peak energy efficiency (pJ/FLOP). - **Decode (Bandwidth-Bound)**: The "Decode" phase requires reading the *entire* model weight set from HBM to generate a single token. Since arithmetic intensity is low, the compute units sit idle for much of the cycle. -This creates **Static Power Waste**: the GPU draws significant leakage and clock power while waiting for memory transfers. Consequently, generating 1,000 tokens through 1,000 sequential decode steps can consume 10–50$\times$ more energy than processing the same 1,000 tokens in a single prefill batch. This inefficiency drives the demand for the specialized, memory-optimized NPUs and TPUs examined in @sec-compute, which prioritize bandwidth-per-watt over raw TFLOPS. +This creates **Static Power Waste**: the GPU draws significant leakage and clock power while waiting for memory transfers. Consequently, generating 1,000 tokens through 1,000 sequential decode steps can consume 10–50 $\times$ more energy than processing the same 1,000 tokens in a single prefill batch. This inefficiency drives the demand for the specialized, memory-optimized NPUs and TPUs examined in @sec-compute, which prioritize bandwidth-per-watt over raw TFLOPS. #### Edge AI Impact {#sec-sustainable-ai-edge-ai-impact-6694} @@ -1647,7 +1647,7 @@ With typical speech activity rates of 5% and wake word occurrence of 0.1%: $$P_{average} = 0.01 + 0.05 \times 0.1 + 0.001 \times 10 \times 0.05 = 0.015 \text{ mW}$$ -This hierarchical approach achieves 15 microwatts average power compared to 10 mW for always-active full inference, a 667$\times$ reduction enabling battery-powered voice assistants with multi-year operation. +This hierarchical approach achieves 15 microwatts average power compared to 10 mW for always-active full inference, a 667 $\times$ reduction enabling battery-powered voice assistants with multi-year operation. **Federated Learning Energy Analysis** @@ -1664,17 +1664,17 @@ The quantitative frameworks developed above, from CMOS power equations through f Carbon footprint analysis provides a crucial but incomplete picture of AI's environmental impact. Comprehensive assessment requires measuring additional ecological impacts including water consumption, hazardous chemical usage, rare material extraction, and biodiversity disruption that often receive less attention despite their ecological significance. -Modern semiconductor fabrication plants producing AI chips require millions of gallons of water daily and use over 250 hazardous substances in their processes. In regions already facing water stress, such as Taiwan, Arizona, and Singapore, this intensive usage threatens local ecosystems and communities. AI hardware also relies heavily on scarce materials like gallium, indium, arsenic, and helium, which face both geopolitical supply risks and depletion concerns. +Modern semiconductor fabrication plants producing AI chips require millions of liters of water daily and use over 250 hazardous substances in their processes. In regions already facing water stress, such as Taiwan, Arizona, and Singapore, this intensive usage threatens local ecosystems and communities. AI hardware also relies heavily on scarce materials like gallium, indium, arsenic, and helium, which face both geopolitical supply risks and depletion concerns. This comprehensive impact assessment enables organizations to identify environmental hotspots beyond energy consumption and develop targeted mitigation strategies that address the full ecological footprint of AI systems. ### Water Usage {#sec-sustainable-ai-water-usage-caae} -Semiconductor fabrication is an exceptionally water-intensive process, requiring vast quantities of ultrapure water for cleaning, cooling, and chemical processing. The scale of water consumption in modern fabs is comparable to that of entire urban populations. For example, TSMC's latest fab in Arizona is projected to consume 8.9 million gallons of water per day [@tsmc2023water][^fn-tsmc-water], accounting for nearly 3% of the city's total water production. This demand places significant strain on local water resources, particularly in water-scarce regions such as Taiwan, Arizona, and Singapore, where semiconductor manufacturing is concentrated. Semiconductor companies have recognized this challenge and are actively investing in recycling technologies and more efficient water management practices. STMicroelectronics, for example, recycles and reuses approximately 41% of its water, significantly reducing its environmental footprint. @fig-water_cycle illustrates the typical semiconductor fab water cycle, showing the stages from raw water intake to wastewater treatment and reuse. +Semiconductor fabrication is an exceptionally water-intensive process, requiring vast quantities of ultrapure water for cleaning, cooling, and chemical processing. The scale of water consumption in modern fabs is comparable to that of entire urban populations. For example, TSMC's latest fab in Arizona is projected to consume 34 million liters of water per day [@tsmc2023water][^fn-tsmc-water], accounting for nearly 3% of the city's total water production. This demand places significant strain on local water resources, particularly in water-scarce regions such as Taiwan, Arizona, and Singapore, where semiconductor manufacturing is concentrated. Semiconductor companies have recognized this challenge and are actively investing in recycling technologies and more efficient water management practices. STMicroelectronics, for example, recycles and reuses approximately 41% of its water, significantly reducing its environmental footprint. @fig-water_cycle illustrates the typical semiconductor fab water cycle, showing the stages from raw water intake to wastewater treatment and reuse. -[^fn-tsmc-water]: **Semiconductor Water Consumption Scale**: TSMC's Arizona facility will consume 3.2 billion gallons annually, equivalent to 37,000 Olympic swimming pools. Each AI chip requires 5-10$\times$ more water than traditional processors due to advanced nodes and complex manufacturing. Intel's Ireland fab uses 1.5 billion gallons annually, while Samsung's Texas facility is projected to use 6 million gallons daily. Water treatment and purification add 30-50% to total consumption. During peak summer months, the cumulative daily water consumption of major fabs rivals that of cities with populations exceeding half a million people. +[^fn-tsmc-water]: **Semiconductor Water Consumption Scale**: TSMC's Arizona facility will consume 12 billion liters annually, equivalent to 37,000 Olympic swimming pools. Each AI chip requires 5-10 $\times$ more water than traditional processors due to advanced nodes and complex manufacturing. Intel's Ireland fab uses 5.7 billion liters annually, while Samsung's Texas facility is projected to use 23 million liters daily. Water treatment and purification add 30-50% to total consumption. During peak summer months, the cumulative daily water consumption of major fabs rivals that of cities with populations exceeding half a million people. -::: {#fig-water_cycle fig-env="figure" fig-pos="htb" fig-cap="**Semiconductor Water Cycle**. Modern fabs consume millions of gallons of water daily. To mitigate this, advanced facilities implement closed-loop recycling. Raw water is purified to Ultra-Pure Water (UPW) for processing. Wastewater is treated and recycled back into the UPW system, reducing net consumption by 60-80%." fig-alt="Flowchart of fab water cycle with 5 stages: raw water source, ultra-pure water purification, fab processing, wastewater treatment, and recycling plant. Blue arrow shows 60-80 percent reclaim loop. Dashed line shows discharge."} +::: {#fig-water_cycle fig-env="figure" fig-pos="htb" fig-cap="**Semiconductor Water Cycle**. Modern fabs consume millions of liters of water daily. To mitigate this, advanced facilities implement closed-loop recycling. Raw water is purified to Ultra-Pure Water (UPW) for processing. Wastewater is treated and recycled back into the UPW system, reducing net consumption by 60-80%." fig-alt="Flowchart of fab water cycle with 5 stages: raw water source, ultra-pure water purification, fab processing, wastewater treatment, and recycling plant. Blue arrow shows 60-80 percent reclaim loop. Dashed line shows discharge."} ```{.tikz} \begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}, node distance=1.5cm] \definecolor{WaterColor}{RGB}{200,240,255} @@ -1709,7 +1709,7 @@ The impact of this massive water usage extends beyond consumption. Excessive wat Daily water footprints across industrial sectors reveal high-tech infrastructure's immense consumption. @fig-water_footprint compares data center water usage with other major industrial applications, demonstrating that a single hyperscale facility can consume more water daily than the irrigation needs of substantial farmland. -![**Data Center Water Usage**: High-density computing infrastructure, such as data centers, consumes substantial water resources for cooling, exceeding many common industrial and agricultural applications. Understanding these water demands is important for designing sustainable AI systems and mitigating potential impacts like *saltwater intrusion* in water-stressed regions. Source: [@google2023cooling].](images/png/water_footprint.png){#fig-water_footprint fig-alt="Three-panel infographic showing 450,000 gallons daily water equivalence: operating a Google data center, watering 17 acres of lawn grass, or producing 160 pairs of cotton jeans."} +![**Data Center Water Usage**: High-density computing infrastructure, such as data centers, consumes substantial water resources for cooling, exceeding many common industrial and agricultural applications. Understanding these water demands is important for designing sustainable AI systems and mitigating potential impacts like *saltwater intrusion* in water-stressed regions. Source: [@google2023cooling].](images/png/water_footprint.png){#fig-water_footprint fig-alt="Three-panel infographic showing 1.7 million liters (450,000 gallons) daily water equivalence: operating a Google data center, watering 7 hectares of lawn grass, or producing 160 pairs of cotton jeans."} While some semiconductor manufacturers implement water recycling systems, the effectiveness of these measures varies. Intel reports that 97% of its direct water consumption is attributed to fabrication processes [@cooper2011semiconductor], and while water reuse is increasing, the sheer scale of water withdrawals remains an important sustainability challenge. @@ -1741,7 +1741,7 @@ While silicon is abundant and readily available, the fabrication of AI accelerat Although silicon serves as the primary material for semiconductor devices, high-performance AI chips depend on rare elements such as gallium, indium, and arsenic, which are essential for high-speed, low-power electronic components [@chen2006gallium]. Gallium and indium, for example, are widely used in compound semiconductors, particularly for 5G communications, optoelectronics, and AI accelerators. The United States Geological Survey (USGS) has classified indium as a critical material, with global supplies expected to last fewer than 15 years at the current rate of consumption [@davies2011endangered][^fn-indium-supply]. -[^fn-indium-supply]: **Critical Material Scarcity**: Indium production is only 600-800 tons annually worldwide, with China controlling 60% of supply. Prices fluctuate wildly, from $60/kg in 2002 to $1,000/kg in 2005, now around $400/kg. Each smartphone contains 0.3mg of indium; each AI accelerator contains 50-100$\times$ more. At current AI hardware growth rates (40% annually), demand will exceed supply by 2035 without recycling breakthroughs. As AI hardware manufacturing scales, the demand for helium will continue to grow, necessitating more sustainable extraction and recycling practices. +[^fn-indium-supply]: **Critical Material Scarcity**: Indium production is only 600-800 tons annually worldwide, with China controlling 60% of supply. Prices fluctuate wildly, from $60/kg in 2002 to $1,000/kg in 2005, now around $400/kg. Each smartphone contains 0.3mg of indium; each AI accelerator contains 50-100 $\times$ more. At current AI hardware growth rates (40% annually), demand will exceed supply by 2035 without recycling breakthroughs. As AI hardware manufacturing scales, the demand for helium will continue to grow, necessitating more sustainable extraction and recycling practices. Another major concern is helium, a noble gas important for semiconductor cooling, plasma etching, and EUV lithography used in next-generation chip production. Helium is unique in that once released into the atmosphere, it escapes Earth's gravity and is lost forever, making it a non-renewable resource [@davies2011endangered]. The semiconductor industry is one of the largest consumers of helium, and supply shortages have already led to price spikes and disruptions in fabrication processes. @@ -1794,7 +1794,7 @@ Semiconductor fabs and data centers require large tracts of land, often leading The physical expansion of AI infrastructure disrupts wildlife migration patterns, as roads, pipelines, transmission towers, and supply chains fragment natural landscapes. Species that rely on large, connected ecosystems for survival, including migratory birds, large mammals, and pollinators, face increased barriers to movement, reducing genetic diversity and population stability. In regions with dense semiconductor manufacturing, such as Taiwan and South Korea, habitat loss has already been linked to declining biodiversity in affected areas [@hsu2016accumulation]. -The massive water consumption of semiconductor fabs poses serious risks to aquatic ecosystems, particularly in water-stressed regions. Excessive groundwater extraction for AI chip production can lower water tables, affecting local rivers, lakes, and wetlands. In Hsinchu, Taiwan, where fabs draw millions of gallons of water daily, seawater intrusion has been reported in local aquifers, altering water chemistry and making it unsuitable for native fish species and vegetation. +The massive water consumption of semiconductor fabs poses serious risks to aquatic ecosystems, particularly in water-stressed regions. Excessive groundwater extraction for AI chip production can lower water tables, affecting local rivers, lakes, and wetlands. In Hsinchu, Taiwan, where fabs draw millions of liters of water daily, seawater intrusion has been reported in local aquifers, altering water chemistry and making it unsuitable for native fish species and vegetation. Beyond depletion, wastewater discharge from fabs introduces chemical contaminants into natural water systems. While many facilities implement advanced filtration and recycling, even trace amounts of heavy metals, fluorides, and solvents can accumulate in water bodies, bioaccumulating in fish and disrupting aquatic ecosystems. Thermal pollution from data centers, which release heated water back into lakes and rivers, can raise temperatures beyond tolerable levels for native species, affecting oxygen levels and reproductive cycles [@poff2002aquatic]. @@ -1808,7 +1808,7 @@ The environmental consequences of AI hardware manufacturing demonstrate the urge The environmental footprint of AI systems extends beyond energy consumption during model training and inference. A comprehensive assessment of AI's sustainability must consider the entire lifecycle, from the extraction of raw materials used in hardware manufacturing to the eventual disposal of obsolete computing infrastructure. Life Cycle Analysis provides a systematic approach to quantifying the cumulative environmental impact of AI across its four key phases: design, manufacture, use, and disposal.[^fn-lifecycle-assessment] -[^fn-lifecycle-assessment]: **Life Cycle Assessment (LCA)**: Systematic methodology for evaluating environmental impacts throughout a product's entire lifespan, from raw material extraction through manufacturing, use, and disposal. Developed in the 1960s, standardized by ISO 14040/14044. For AI systems, LCA reveals that hardware manufacturing often contributes 30-50% of total emissions despite consuming no operational energy. LCA studies identified that a single NVIDIA H100 GPU generates 300-500 kg CO₂ during production, equivalent to driving 1,200 miles, before any computation occurs. +[^fn-lifecycle-assessment]: **Life Cycle Assessment (LCA)**: Systematic methodology for evaluating environmental impacts throughout a product's entire lifespan, from raw material extraction through manufacturing, use, and disposal. Developed in the 1960s, standardized by ISO 14040/14044. For AI systems, LCA reveals that hardware manufacturing often contributes 30-50% of total emissions despite consuming no operational energy. LCA studies identified that a single NVIDIA H100 GPU generates 300-500 kg CO₂ during production, equivalent to driving 1,900 km, before any computation occurs. By applying LCA to AI systems, researchers and policymakers can pinpoint important intervention points to reduce emissions, improve resource efficiency, and implement sustainable practices. This approach provides a holistic understanding of AI's ecological costs, extending sustainability considerations beyond operational power consumption to include hardware supply chains and electronic waste management. @@ -1995,12 +1995,12 @@ The carbon footprint of AI model design varies significantly depending on the co The correlation between model complexity and environmental impact becomes quantifiable through carbon accounting. @tbl-training-emissions reveals stark differences in emissions across model scales, from relatively modest transformer variants to foundation models that generate hundreds of tons of CO2 equivalent. -| **AI Model** | **Training FLOPs** | **Estimated $\textrm{CO}_2$ Emissions (kg)** | **Equivalent Car Mileage** | -|:----------------|---------------------:|---------------------------------------------:|---------------------------:| -| **GPT-3** | $3.1 \times 10^{23}$ | 502,000 kg | 1.2 million miles | -| **T5-11B** | $2.3 \times 10^{22}$ | 85,000 kg | 210,000 miles | -| **BERT (Base)** | $3.3 \times 10^{18}$ | 650 kg | 1,500 miles | -| **ResNet-50** | $2.0 \times 10^{17}$ | 35 kg | 80 miles | +| **AI Model** | **Training FLOPs** | **Estimated $\textrm{CO}_2$ Emissions (kg)** | **Equivalent Car Distance** | +|:----------------|---------------------:|---------------------------------------------:|----------------------------:| +| **GPT-3** | $3.1 \times 10^{23}$ | 502,000 kg | 1.9 million km | +| **T5-11B** | $2.3 \times 10^{22}$ | 85,000 kg | 338,000 km | +| **BERT (Base)** | $3.3 \times 10^{18}$ | 650 kg | 2,400 km | +| **ResNet-50** | $2.0 \times 10^{17}$ | 35 kg | 129 km | : **Model Carbon Footprint**: Training large AI models generates substantial carbon emissions, directly correlating with computational demands measured in flops; for example, training GPT-3 requires energy equivalent to the lifetime emissions of hundreds of cars. Understanding these emissions is important for developing sustainable AI practices and selecting energy-efficient hardware like tpus to minimize environmental impact. Source: {#tbl-training-emissions} @@ -2018,7 +2018,7 @@ The manufacturing phase of AI systems represents a resource-intensive aspect of The foundation of AI hardware lies in semiconductors, primarily silicon-based integrated circuits that power AI accelerators. However, modern AI chips rely on more than just silicon. They require specialty materials such as gallium, indium, arsenic, and helium, each of which carries unique environmental extraction costs. These materials are often classified as important elements due to their scarcity, geopolitical sensitivity, and high energy costs associated with mining and refining [@nakano2021geopolitics]. -Silicon itself is abundant, but refining it into high-purity wafers requires extensive energy-intensive processes. The production of a single 300mm silicon wafer requires over 8,300 liters of water, along with strong acids such as hydrofluoric acid, sulfuric acid, and nitric acid used for etching and cleaning [@cope2009pure]. The demand for ultra-pure water in semiconductor fabrication places a significant burden on local water supplies, with leading fabs consuming millions of gallons per day. +Silicon itself is abundant, but refining it into high-purity wafers requires extensive energy-intensive processes. The production of a single 300mm silicon wafer requires over 8,300 liters of water, along with strong acids such as hydrofluoric acid, sulfuric acid, and nitric acid used for etching and cleaning [@cope2009pure]. The demand for ultra-pure water in semiconductor fabrication places a significant burden on local water supplies, with leading fabs consuming millions of liters per day. Beyond silicon, gallium and indium are important for high-performance compound semiconductors, such as those used in high-speed AI accelerators and 5G communications. The U.S. Geological Survey has classified indium as a critically endangered material, with global supplies estimated to last fewer than 15 years at current consumption rates [@davies2011endangered]. Meanwhile, helium, an essential cooling agent in chip production, is a non-renewable resource that, once released, escapes Earth's gravity, making it permanently unrecoverable. The continued expansion of AI hardware manufacturing is accelerating the depletion of these critical elements, raising concerns about long-term sustainability. @@ -2032,7 +2032,7 @@ The energy required to manufacture AI hardware is substantial, with the total en Semiconductor fabrication also generates large volumes of hazardous waste, including gaseous emissions, VOCs, chemical wastewater, and solid byproducts. The acids and solvents used in chip production produce toxic waste streams that require specialized handling to prevent contamination of surrounding ecosystems. Despite advancements in wastewater treatment, trace amounts of metals and chemical residues can still be released into rivers and lakes, affecting aquatic biodiversity and human health [@prakash2022cfu]. -The demand for water in semiconductor fabs has also raised concerns about regional water stress. The TSMC fab in Arizona is projected to consume 8.9 million gallons per day, a figure that accounts for nearly 3% of the city's water supply. While some fabs have begun investing in water recycling systems, these efforts remain insufficient to offset the growing demand. +The demand for water in semiconductor fabs has also raised concerns about regional water stress. The TSMC fab in Arizona is projected to consume 34 million liters per day, a figure that accounts for nearly 3% of the city's water supply. While some fabs have begun investing in water recycling systems, these efforts remain insufficient to offset the growing demand. #### Sustainable Initiatives {#sec-sustainable-ai-sustainable-initiatives-145c} @@ -2102,7 +2102,7 @@ Addressing AI's environmental footprint requires a multi-layered approach that i This section explores key strategies for mitigating AI's environmental impact, beginning with sustainable AI development principles. @fig-jevons-ai captures the core challenge: efficiency improvements that reduce per-unit energy often trigger demand increases that overwhelm the savings, a phenomenon known as the Jevons paradox. -[^fn-jevons-paradox]: **Jevon's Paradox**: Named after British economist William Stanley Jevons who observed in 1865 [@jevons1865coal] that improving coal efficiency actually increased total coal consumption rather than reducing it. Modern examples include LEDs—despite being 85% more efficient than incandescent bulbs, total lighting energy consumption has increased due to expanded usage. In AI, this means that making models 10$\times$ more efficient might lead to 100$\times$ more AI applications, resulting in net increase in environmental impact. +[^fn-jevons-paradox]: **Jevon's Paradox**: Named after British economist William Stanley Jevons who observed in 1865 [@jevons1865coal] that improving coal efficiency actually increased total coal consumption rather than reducing it. Modern examples include LEDs—despite being 85% more efficient than incandescent bulbs, total lighting energy consumption has increased due to expanded usage. In AI, this means that making models 10 $\times$ more efficient might lead to 100 $\times$ more AI applications, resulting in net increase in environmental impact. As AI systems become more efficient, the cost per unit of computation decreases, whether for language model tokens, computer vision inferences, or recommendation system predictions. Moving from point A to point B represents a drop in computation cost. However, this price reduction leads to increased usage across all AI applications, with corresponding shift from point C to point D on the horizontal axis. While there are savings from reduced costs, the total consumption of AI services increases even more rapidly, ultimately resulting in higher overall resource usage and environmental impact. This dynamic highlights the core of Jevon's Paradox in AI: efficiency alone is not sufficient to guarantee sustainability. @@ -2156,7 +2156,7 @@ The paradox has profound implications for sustainable AI strategy. Test your und ::: {.callout-note appearance="simple"} ## Knowledge Check: Jevons Paradox -True or False: Making AI models 10$\times$ more energy-efficient guarantees a reduction in total global energy consumption for AI. +True or False: Making AI models 10 $\times$ more energy-efficient guarantees a reduction in total global energy consumption for AI. - [ ] True - [x] False @@ -2196,13 +2196,13 @@ efficiency_range = cpu_pj_per_flop / asic_pj_per_flop efficiency_range_str = f"{efficiency_range:.0f}" ``` -[^fn-flops-vs-flops]: **FLOPS vs FLOPs**: FLOPS (all caps) = Floating-Point Operations Per Second (rate), while FLOPs (mixed case) = total Floating-Point Operations (count). GPT-3 training required 3.1$\times$10²³ FLOPs total, executed on hardware capable of 1.25$\times$10¹⁷ FLOPS. Energy efficiency varies dramatically across hardware: CPUs consume ~`{python} cpu_pj_per_flop` pJ/FLOP, GPUs achieve ~`{python} gpu_pj_per_flop` pJ/FLOP, TPUs reach ~`{python} tpu_pj_per_flop` pJ/FLOP, while specialized AI accelerators approach `{python} asic_pj_per_flop` pJ/FLOP—a `{python} efficiency_range_str`$\times$ efficiency range that defines sustainability opportunities. +[^fn-flops-vs-flops]: **FLOPS vs FLOPs**: FLOPS (all caps) = Floating-Point Operations Per Second (rate), while FLOPs (mixed case) = total Floating-Point Operations (count). GPT-3 training required 3.1 $\times$ 10²³ FLOPs total, executed on hardware capable of 1.25 $\times$ 10¹⁷ FLOPS. Energy efficiency varies dramatically across hardware: CPUs consume ~`{python} cpu_pj_per_flop` pJ/FLOP, GPUs achieve ~`{python} gpu_pj_per_flop` pJ/FLOP, TPUs reach ~`{python} tpu_pj_per_flop` pJ/FLOP, while specialized AI accelerators approach `{python} asic_pj_per_flop` pJ/FLOP—a `{python} efficiency_range_str` $\times$ efficiency range that defines sustainability opportunities. Model pruning provides a widely used method for improving energy efficiency by removing unnecessary connections from trained models.[^fn-pruning-technique] By systematically eliminating redundant weights, pruning reduces both the model size and the number of computations required during inference. Studies show that structured pruning can remove up to 90 percent of weights in models such as ResNet-50 while maintaining comparable accuracy. This approach allows AI models to operate efficiently on lower-power hardware, making them more suitable for deployment in resource-constrained environments. -[^fn-pruning-technique]: Pruning fundamentals are covered in model optimization references. For sustainability, the key insight is that structured pruning at 90% sparsity can reduce inference energy by 2-10$\times$ with minimal accuracy impact. LLMs like SparseGPT achieve 60% sparsity with <1% accuracy loss, reducing inference memory and compute proportionally. +[^fn-pruning-technique]: Pruning fundamentals are covered in model optimization references. For sustainability, the key insight is that structured pruning at 90% sparsity can reduce inference energy by 2-10 $\times$ with minimal accuracy impact. LLMs like SparseGPT achieve 60% sparsity with <1% accuracy loss, reducing inference memory and compute proportionally. -Another technique for reducing energy consumption is quantization, which lowers the numerical precision of computations in AI models.[^fn-quantization-technique] Standard deep learning models typically use 32-bit floating-point precision, but many operations can be performed with 8-bit or even 4-bit integers without significant accuracy loss. The energy efficiency gains from quantization are substantial. 8-bit integer operations consume approximately 16$\times$ less energy than 32-bit floating-point operations, while 4-bit operations achieve 64$\times$ energy reductions. This hardware-software co-design optimization requires careful coordination between algorithm precision requirements and hardware capabilities. By using lower precision, quantization reduces memory requirements, speeds up inference, and lowers power consumption. NVIDIA's TensorRT framework applies post-training quantization to deep learning models, achieving a threefold increase in inference speed while maintaining nearly identical accuracy. Similarly, Intel's Q8BERT demonstrates that quantizing the BERT language model to 8-bit integers can reduce its size by a factor of four with minimal performance degradation [@zafrir2019q8bert]. +Another technique for reducing energy consumption is quantization, which lowers the numerical precision of computations in AI models.[^fn-quantization-technique] Standard deep learning models typically use 32-bit floating-point precision, but many operations can be performed with 8-bit or even 4-bit integers without significant accuracy loss. The energy efficiency gains from quantization are substantial. 8-bit integer operations consume approximately 16 $\times$ less energy than 32-bit floating-point operations, while 4-bit operations achieve 64 $\times$ energy reductions. This hardware-software co-design optimization requires careful coordination between algorithm precision requirements and hardware capabilities. By using lower precision, quantization reduces memory requirements, speeds up inference, and lowers power consumption. NVIDIA's TensorRT framework applies post-training quantization to deep learning models, achieving a threefold increase in inference speed while maintaining nearly identical accuracy. Similarly, Intel's Q8BERT demonstrates that quantizing the BERT language model to 8-bit integers can reduce its size by a factor of four with minimal performance degradation [@zafrir2019q8bert]. [^fn-quantization-technique]: **Quantization Technique**: Reducing numerical precision (FP32→INT8/INT4) to improve efficiency, with calibration preserving accuracy. Post-training quantization requires calibration datasets; quantization-aware training achieves <0.5% accuracy loss at INT8. GPTQ enables 4-bit LLM quantization with 2% perplexity increase, reducing LLaMA-65B memory from 130GB to 32GB for consumer GPU deployment. @@ -2236,13 +2236,13 @@ Match the technique to its mechanism for saving energy: **TinyML Optimization Stack** -TinyML deployments face unique constraints beyond datacenter optimization: models must fit in kilobytes of SRAM, execute with microsecond latency, and consume milliwatts of power. Standard optimization techniques like INT8 quantization (4$\times$ memory reduction, 8-16$\times$ energy savings) and structured pruning (2-10$\times$ improvements at 90% sparsity) provide the foundation for microcontroller deployment. However, achieving sustainable operation on energy-harvesting devices requires pushing optimization to extremes. This section examines techniques that enable truly autonomous TinyML systems operating on harvested energy budgets of 10-100 microwatts, as summarized in @tbl-tinyml-optimization. +TinyML deployments face unique constraints beyond datacenter optimization: models must fit in kilobytes of SRAM, execute with microsecond latency, and consume milliwatts of power. Standard optimization techniques like INT8 quantization (4 $\times$ memory reduction, 8-16 $\times$ energy savings) and structured pruning (2-10 $\times$ improvements at 90% sparsity) provide the foundation for microcontroller deployment. However, achieving sustainable operation on energy-harvesting devices requires pushing optimization to extremes. This section examines techniques that enable truly autonomous TinyML systems operating on harvested energy budgets of 10-100 microwatts, as summarized in @tbl-tinyml-optimization. -| **Technique** | **Typical Accuracy** **Impact** | **Memory Reduction** | **Energy Reduction** | -|:---------------------------|:--------------------------------|:---------------------|----------------------------:| -| **Binary Neural Networks** | 5-15% | 32x | 50-100x | -| **Neural Architecture** | varies | task-dependent | 2-5$\times$ versus baseline | -| **Search for MCUs** | | | | +| **Technique** | **Typical Accuracy** **Impact** | **Memory Reduction** | **Energy Reduction** | +|:---------------------------|:--------------------------------|:---------------------|-----------------------------:| +| **Binary Neural Networks** | 5-15% | 32x | 50-100x | +| **Neural Architecture** | varies | task-dependent | 2-5 $\times$ versus baseline | +| **Search for MCUs** | | | | : **Extreme TinyML Optimization Techniques**: For energy-harvesting devices operating on microwatt budgets, these techniques push beyond conventional INT8/pruning approaches, trading significant accuracy for the dramatic efficiency gains required for truly autonomous operation. {#tbl-tinyml-optimization} @@ -2255,7 +2255,7 @@ TinyML deployments face unique constraints beyond datacenter optimization: model *Binary Neural Networks for Energy Harvesting:* For devices powered by ambient energy harvesting (solar, vibration, RF), even INT8 inference may exceed available power budgets. Binary neural networks (BNNs) push quantization to its extreme, representing weights and activations as single bits. This directly enables the ultra-low-power operation required for the **TinyML** paradigms established in @sec-edge-intelligence. -- **XNOR-Net operations**: Replace multiply-accumulate with bit operations, achieving 50-100$\times$ energy reduction over full-precision inference +- **XNOR-Net operations**: Replace multiply-accumulate with bit operations, achieving 50-100 $\times$ energy reduction over full-precision inference - **Sub-milliwatt inference**: Enable always-on sensing on harvested energy budgets of 10-100 microwatts - **Accuracy trade-offs**: BNNs sacrifice 5-15% accuracy compared to full-precision models, acceptable for many classification tasks where sustainability outweighs precision requirements @@ -2314,7 +2314,7 @@ Examine @tbl-mlperf-tiny to understand the benchmark tasks and their typical ene | **Anomaly Detection** | Time Series (machine health) | Deep Autoencoder (5 KB) | 0.01-0.1 mJ | | **Image Classification** | Visual Recognition (CIFAR-10) | ResNet-8 (70 KB) | 0.5-5.0 mJ | -: **MLPerf Tiny Benchmark Suite**: Standardized benchmarks for TinyML systems measure accuracy, latency, and energy consumption on microcontroller-class hardware. Reference model sizes indicate minimum viable deployments; optimized implementations often achieve 2-10$\times$ better energy efficiency. {#tbl-mlperf-tiny} +: **MLPerf Tiny Benchmark Suite**: Standardized benchmarks for TinyML systems measure accuracy, latency, and energy consumption on microcontroller-class hardware. Reference model sizes indicate minimum viable deployments; optimized implementations often achieve 2-10 $\times$ better energy efficiency. {#tbl-mlperf-tiny} **Energy Delay Product** @@ -2352,7 +2352,7 @@ A promising approach to reducing data center emissions is the transition to rene Cooling systems represent another major contributor to the energy footprint of data centers, often accounting for 30-40% of total electricity consumption[^fn-cooling-energy]. Traditional cooling methods rely on air conditioning units and mechanical chillers, both of which require significant power and water resources. -[^fn-cooling-energy]: **Data Center Cooling Costs**: Cooling consumes 38% of total data center energy on average. A typical 10 MW data center spends $3.8 million annually on cooling electricity. Google's machine learning optimization reduced cooling energy by 40%, saving $150+ million globally. Liquid cooling can be 3,000$\times$ more efficient than air cooling for high-density AI workloads, reducing cooling energy from 40% to under 10% of total consumption. To improve efficiency, data centers are adopting alternative cooling strategies that reduce energy waste. Liquid cooling, which transfers heat away from AI accelerators using specially designed coolant systems, is significantly more effective than traditional air cooling and is now being deployed in high-density computing clusters. Free-air cooling, which utilizes natural airflow instead of mechanical refrigeration, has also been adopted in temperate climates, where external conditions allow for passive cooling. Microsoft has taken this a step further by deploying underwater data centers that use the surrounding ocean as a natural cooling mechanism, reducing the need for active temperature regulation. +[^fn-cooling-energy]: **Data Center Cooling Costs**: Cooling consumes 38% of total data center energy on average. A typical 10 MW data center spends $3.8 million annually on cooling electricity. Google's machine learning optimization reduced cooling energy by 40%, saving $150+ million globally. Liquid cooling can be 3,000 $\times$ more efficient than air cooling for high-density AI workloads, reducing cooling energy from 40% to under 10% of total consumption. To improve efficiency, data centers are adopting alternative cooling strategies that reduce energy waste. Liquid cooling, which transfers heat away from AI accelerators using specially designed coolant systems, is significantly more effective than traditional air cooling and is now being deployed in high-density computing clusters. Free-air cooling, which utilizes natural airflow instead of mechanical refrigeration, has also been adopted in temperate climates, where external conditions allow for passive cooling. Microsoft has taken this a step further by deploying underwater data centers that use the surrounding ocean as a natural cooling mechanism, reducing the need for active temperature regulation. Beyond hardware-level optimizations, AI itself is being used to improve the energy efficiency of data center operations. DeepMind has developed machine learning algorithms capable of dynamically adjusting cooling parameters based on real-time sensor data. These AI-powered cooling systems analyze temperature, humidity, and fan speeds, making continuous adjustments to optimize energy efficiency. When deployed in Google's data centers, DeepMind's system achieved a 40 percent reduction in cooling energy consumption, demonstrating the potential of AI to enhance the sustainability of the infrastructure that supports machine learning workloads. @@ -2397,7 +2397,7 @@ The potential for carbon-aware scheduling extends beyond hyperscale cloud provid The effectiveness of carbon-aware AI scheduling depends on accurate real-time data about grid emissions. Electricity providers and sustainability organizations have begun publishing grid carbon intensity data through publicly available APIs, allowing AI systems to dynamically respond to changes in energy supply. For instance, the Electricity Maps API provides real-time CO₂ emissions data for power grids worldwide[^fn-grid-carbon-data], enabling AI infrastructure to adjust computational workloads based on carbon availability. As access to grid emissions data improves, carbon-aware computing will become a scalable and widely adoptable solution for reducing the environmental impact of AI operations. -[^fn-grid-carbon-data]: **Real-Time Grid Carbon Intensity**: Grid carbon intensity varies dramatically—from 50g CO₂/kWh in nuclear-heavy France to 820g/kWh in coal-dependent Poland. In Texas, intensity fluctuates 10$\times$ daily (150-1,500g/kWh) based on wind generation. The Electricity Maps API serves 50+ million requests daily to allow carbon-aware computing. WattTime API provides marginal emissions data showing which power plants turn on/off next, allowing 2-5$\times$ better carbon optimization than average intensity. +[^fn-grid-carbon-data]: **Real-Time Grid Carbon Intensity**: Grid carbon intensity varies dramatically—from 50g CO₂/kWh in nuclear-heavy France to 820g/kWh in coal-dependent Poland. In Texas, intensity fluctuates 10 $\times$ daily (150-1,500g/kWh) based on wind generation. The Electricity Maps API serves 50+ million requests daily to allow carbon-aware computing. WattTime API provides marginal emissions data showing which power plants turn on/off next, allowing 2-5 $\times$ better carbon optimization than average intensity. By shifting AI computations to times and places with cleaner energy sources, carbon-aware scheduling represents a powerful tool for making AI infrastructure more sustainable. Unlike hardware-based optimizations that require physical upgrades, scheduling improvements can be implemented through software, offering an immediate and cost-effective pathway to emissions reductions. As more organizations integrate carbon-aware scheduling into their AI workflows, the cumulative impact on reducing global AI-related carbon emissions could be substantial. @@ -2413,7 +2413,7 @@ Beyond scheduling, optimizing inference sustainability requires complementary ha Software frameworks specifically designed for energy efficiency provide additional optimization opportunities. Energy-aware AI frameworks, such as Zeus [@jie2023zeus] and Perseus [@jaewon2023perseus][^fn-energy-frameworks], balance computational speed and power efficiency during both training and inference. These platforms optimize model execution by analyzing trade-offs between speed and energy consumption, facilitating widespread adoption of energy-efficient AI strategies, particularly for inference operations that must run continuously at scale. -[^fn-energy-frameworks]: **Energy-Aware AI Frameworks**: Zeus framework achieves 75% energy savings on BERT training by automatically finding optimal energy-performance trade-offs. Perseus reduces GPU memory usage by 50% through dynamic batching, lowering energy consumption proportionally. CodeCarbon automatically tracks emissions, revealing that training can vary 10-100$\times$ in energy usage depending on optimization settings. These tools democratize energy optimization beyond just hyperscale companies. +[^fn-energy-frameworks]: **Energy-Aware AI Frameworks**: Zeus framework achieves 75% energy savings on BERT training by automatically finding optimal energy-performance trade-offs. Perseus reduces GPU memory usage by 50% through dynamic batching, lowering energy consumption proportionally. CodeCarbon automatically tracks emissions, revealing that training can vary 10-100 $\times$ in energy usage depending on optimization settings. These tools democratize energy optimization beyond just hyperscale companies. #### AI-Driven Thermal Optimization {#sec-sustainable-ai-aidriven-thermal-optimization-68ef} @@ -2433,7 +2433,7 @@ AI-driven cooling and thermal management represent an immediate and scalable opp As AI systems continue to scale, efforts to mitigate their environmental impact have largely focused on improving energy efficiency in model design and optimizing data center infrastructure. While these advancements are important, they only address part of the problem. AI's environmental impact extends far beyond operational energy use, encompassing everything from the water consumption in semiconductor manufacturing to the growing burden of electronic waste. A truly sustainable AI ecosystem must account for the full life cycle of AI hardware and software, integrating sustainability at every stage—from material sourcing to disposal. -Our exploration of the LCA of AI systems highlights the substantial carbon emissions, water consumption, and material waste associated with AI hardware manufacturing and deployment. Many of these environmental costs are embedded in the supply chain and do not appear in operational energy reports, leading to an incomplete picture of AI's true sustainability. Data centers remain water-intensive, with cooling systems consuming millions of gallons per day, and AI accelerators are often refreshed on short life cycles, leading to mounting e-waste. +Our exploration of the LCA of AI systems highlights the substantial carbon emissions, water consumption, and material waste associated with AI hardware manufacturing and deployment. Many of these environmental costs are embedded in the supply chain and do not appear in operational energy reports, leading to an incomplete picture of AI's true sustainability. Data centers remain water-intensive, with cooling systems consuming millions of liters per day, and AI accelerators are often refreshed on short life cycles, leading to mounting e-waste. This section builds on those discussions by examining how AI's broader environmental footprint can be reduced. We explore strategies to mitigate AI's supply chain impact, curb water consumption, and extend hardware longevity. Moving beyond optimizing infrastructure, this approach takes a holistic view of AI sustainability, ensuring that improvements are not just localized to energy efficiency but embedded throughout the entire AI ecosystem. @@ -2443,7 +2443,7 @@ AI's environmental footprint extends far beyond electricity consumption during m LCA studies reveal the substantial embodied carbon[^fn-embodied-carbon] cost of AI hardware. Unlike operational emissions, which can be reduced by shifting to cleaner energy sources, embodied emissions result from the raw material extraction, semiconductor fabrication, and supply chain logistics that precede an AI accelerator's deployment. -AI's water consumption has often been overlooked in sustainability discussions. Semiconductor fabrication plants, in which AI accelerators are produced, are among the most water-intensive industrial facilities in the world, consuming millions of gallons daily for wafer cleaning and chemical processing. Data centers, too, rely on large amounts of water for cooling, with some hyperscale facilities using as much as 450,000 gallons per day, a number that continues to rise as AI workloads become more power-dense. Given that many of the world's chip manufacturing hubs are located in water-stressed regions, such as Taiwan and Arizona, AI's dependence on water raises serious sustainability concerns. +AI's water consumption has often been overlooked in sustainability discussions. Semiconductor fabrication plants, in which AI accelerators are produced, are among the most water-intensive industrial facilities in the world, consuming millions of liters daily for wafer cleaning and chemical processing. Data centers, too, rely on large amounts of water for cooling, with some hyperscale facilities using as much as 1.7 million liters per day, a number that continues to rise as AI workloads become more power-dense. Given that many of the world's chip manufacturing hubs are located in water-stressed regions, such as Taiwan and Arizona, AI's dependence on water raises serious sustainability concerns. Beyond emissions and water use, AI hardware also contributes to a growing e-waste problem. The rapid evolution of AI accelerators has led to short hardware refresh cycles, where GPUs and TPUs are frequently replaced with newer, more efficient versions. While improving efficiency is important, discarding functional hardware after only a few years leads to unnecessary electronic waste and resource depletion. Many AI chips contain rare earth metals and toxic components, which, if not properly recycled, can contribute to environmental pollution. @@ -2495,13 +2495,13 @@ Achieving systemic change in AI sustainability requires a multi-stakeholder appr To mitigate emissions from rapidly expanding AI workloads, Google engineers identified four key optimization areas, identified as the "4 Ms," where systematic improvements collectively reduce the carbon footprint of machine learning [@patterson2022carbon]: -* **Model**: The selection of efficient AI architectures reduces computation requirements by 5-10$\times$ without compromising model quality. Google has extensively researched sparse models and neural architecture search methodologies, resulting in efficient architectures such as the Evolved Transformer and Primer. +* **Model**: The selection of efficient AI architectures reduces computation requirements by 5-10 $\times$ without compromising model quality. Google has extensively researched sparse models and neural architecture search methodologies, resulting in efficient architectures such as the Evolved Transformer and Primer. -* **Machine**: The implementation of AI-specific hardware offers 2-5$\times$ improvements in performance per watt compared to general-purpose systems. Google's TPUs demonstrate 5-13$\times$ greater carbon efficiency relative to non-optimized GPUs. +* **Machine**: The implementation of AI-specific hardware offers 2-5 $\times$ improvements in performance per watt compared to general-purpose systems. Google's TPUs demonstrate 5-13 $\times$ greater carbon efficiency relative to non-optimized GPUs. -* **Mechanization**: The utilization of optimized cloud computing infrastructure with high utilization rates yields 1.4-2$\times$ energy reductions compared to conventional on-premise data centers. Google's facilities consistently exceed industry standards for PUE. +* **Mechanization**: The utilization of optimized cloud computing infrastructure with high utilization rates yields 1.4-2 $\times$ energy reductions compared to conventional on-premise data centers. Google's facilities consistently exceed industry standards for PUE. -* **Map**: The strategic positioning of data centers in regions with low-carbon electricity supplies reduces gross emissions by 5-10$\times$. Google maintains real-time monitoring of renewable energy usage across its global infrastructure. +* **Map**: The strategic positioning of data centers in regions with low-carbon electricity supplies reduces gross emissions by 5-10 $\times$. Google maintains real-time monitoring of renewable energy usage across its global infrastructure. The combined effect of these practices produces multiplicative efficiency gains. For instance, implementing the optimized Transformer model on TPUs in strategically located data centers reduced energy consumption by a factor of 83 and CO₂ emissions by a factor of 747. @@ -2509,7 +2509,7 @@ Despite substantial growth in AI deployment across Google's product ecosystem, s Empirical case studies demonstrate how engineering principles focused on sustainable AI development allow simultaneous improvements in both performance and environmental impact. For example, comparative analysis between GPT-3 (considered state-of-the-art in mid-2020) and Google's GLaM model reveals improved accuracy metrics alongside reduced training computation requirements and lower-carbon energy sources—resulting in a 14-fold reduction in CO₂ emissions within an 18-month development cycle. -Google's analysis indicates that previous published estimates overestimated machine learning's energy requirements by factors ranging from 100 to 100,000$\times$ due to methodological limitations and absence of empirical measurements. Through transparent reporting of optimization metrics, Google provides a factual basis for efficiency initiatives while correcting disproportionate projections regarding machine learning's environmental impact. +Google's analysis indicates that previous published estimates overestimated machine learning's energy requirements by factors ranging from 100 to 100,000 $\times$ due to methodological limitations and absence of empirical measurements. Through transparent reporting of optimization metrics, Google provides a factual basis for efficiency initiatives while correcting disproportionate projections regarding machine learning's environmental impact. While substantial progress has been achieved in constraining the carbon footprint of AI operations, Google acknowledges that continued efficiency advancements are important for responsible innovation as AI applications proliferate. Their ongoing optimization framework encompasses: @@ -2653,7 +2653,7 @@ Planned obsolescence also affects industrial AI hardware, including AI-powered c Planned obsolescence is not just a financial burden on consumers; it has severe environmental consequences. By shortening product lifespans and discouraging repairability, manufacturers increase the demand for new electronic components, leading to higher resource extraction, energy consumption, and carbon emissions. -The impact of this cycle is particularly concerning given the high environmental cost of semiconductor manufacturing. Producing AI chips, GPUs, and other advanced computing components requires vast amounts of water, rare earth minerals, and energy. For example, a single 5nm semiconductor fabrication plant consumes millions of gallons of ultrapure water daily and relies on energy-intensive processes that generate significant CO₂ emissions [@mills1997overview; @harris2023semiconductor]. When AI-powered devices are discarded prematurely, the environmental cost of manufacturing is effectively wasted, amplifying AI's overall sustainability challenges. +The impact of this cycle is particularly concerning given the high environmental cost of semiconductor manufacturing. Producing AI chips, GPUs, and other advanced computing components requires vast amounts of water, rare earth minerals, and energy. For example, a single 5nm semiconductor fabrication plant consumes millions of liters of ultrapure water daily and relies on energy-intensive processes that generate significant CO₂ emissions [@mills1997overview; @harris2023semiconductor]. When AI-powered devices are discarded prematurely, the environmental cost of manufacturing is effectively wasted, amplifying AI's overall sustainability challenges. Additionally, many discarded AI devices contain hazardous materials, including lead, mercury, and brominated flame retardants, which can leach into the environment if not properly recycled [@puckett2016e-waste]. The acceleration of AI-powered consumer electronics and industrial hardware turnover will only worsen the global e-waste crisis, further straining waste management and recycling systems. @@ -2856,7 +2856,7 @@ The governance frameworks, public engagement strategies, and equity consideratio A major priority in AI sustainability is the development of more energy-efficient models and algorithms. Optimizing deep learning models to minimize computational cost is a key research direction, with techniques such as model pruning, quantization, and low-precision numerics demonstrating significant potential for reducing energy consumption without compromising performance. These strategies aim to improve the efficiency of AI workloads while leveraging specialized hardware accelerators to maximize computational throughput with minimal energy expenditure. The continued development of non-von Neumann computing[^fn-von-neumann] paradigms, such as neuromorphic computing and in-memory computing, presents another avenue for energy-efficient AI architectures, through specialized hardware designs. -[^fn-von-neumann]: **Von Neumann Architecture**: Traditional computing model where processing unit and memory are separate, requiring constant data movement between CPU and RAM. Proposed by John von Neumann in 1945, dominates modern computers but creates the "von Neumann bottleneck"—energy-intensive data shuttling that consumes 60-80% of system power. Non-von Neumann approaches like neuromorphic chips, in-memory computing, and dataflow architectures eliminate this bottleneck by processing data where it's stored, potentially reducing AI energy consumption by 100-1000$\times$. +[^fn-von-neumann]: **Von Neumann Architecture**: Traditional computing model where processing unit and memory are separate, requiring constant data movement between CPU and RAM. Proposed by John von Neumann in 1945, dominates modern computers but creates the "von Neumann bottleneck"—energy-intensive data shuttling that consumes 60-80% of system power. Non-von Neumann approaches like neuromorphic chips, in-memory computing, and dataflow architectures eliminate this bottleneck by processing data where it's stored, potentially reducing AI energy consumption by 100-1000 $\times$. Another important direction involves the integration of renewable energy into AI infrastructure. Given that data centers are among the largest contributors to AI's carbon footprint, shifting towards clean energy sources like solar, wind, and hydroelectric power is imperative. The feasibility of this transition depends on advancements in sustainable energy storage technologies, such as those being developed by companies like Ambri, an MIT spinoff working on liquid metal battery solutions. These innovations could allow data centers to operate on renewable energy with greater reliability, reducing dependency on fossil fuel-based grid power. However, achieving this transition at scale requires collaborative efforts between AI companies, energy providers, and policymakers to develop grid-aware AI scheduling and carbon-aware workload management strategies, ensuring that compute-intensive AI tasks are performed when renewable energy availability is at its peak. @@ -2886,7 +2886,7 @@ Sustainability involves counterintuitive physics where efficiency improvements c **Fallacy:** ***Cloud computing automatically makes AI systems more environmentally sustainable.*** -Engineers assume cloud providers operate efficiently and sustainably. In production, geographic region dominates all other factors through grid carbon intensity differences. Training a 7B model on 64 A100s for 14 days produces 4.4 metric tons CO₂ on the US average grid (367 g/kWh) but only 206 kg CO₂ in Quebec's hydroelectric grid (34.5 g/kWh lifecycle)—a 21-fold difference for identical workloads. Coal-powered grids emit 800-1000 g CO₂/kWh while well-managed hydroelectric sources emit 10-50 g CO₂/kWh. As demonstrated in @sec-sustainable-ai-carbon-footprint-calculation-c2fe, teams that deploy to default cloud regions without checking grid carbon intensity waste 20-50$\times$ more carbon budget than necessary, turning "cloud sustainability" into a geographic lottery rather than an inherent advantage. +Engineers assume cloud providers operate efficiently and sustainably. In production, geographic region dominates all other factors through grid carbon intensity differences. Training a 7B model on 64 A100s for 14 days produces 4.4 metric tons CO₂ on the US average grid (367 g/kWh) but only 206 kg CO₂ in Quebec's hydroelectric grid (34.5 g/kWh lifecycle)—a 21-fold difference for identical workloads. Coal-powered grids emit 800-1000 g CO₂/kWh while well-managed hydroelectric sources emit 10-50 g CO₂/kWh. As demonstrated in @sec-sustainable-ai-carbon-footprint-calculation-c2fe, teams that deploy to default cloud regions without checking grid carbon intensity waste 20-50 $\times$ more carbon budget than necessary, turning "cloud sustainability" into a geographic lottery rather than an inherent advantage. **Pitfall:** ***Focusing only on operational energy consumption while ignoring embodied carbon and lifecycle impacts.*** @@ -2894,15 +2894,15 @@ Teams optimize training efficiency while ignoring manufacturing emissions. In lo **Fallacy:** ***Efficiency improvements automatically reduce total environmental impact.*** -Engineers assume that halving inference cost cuts environmental impact in half. In production, Jevons Paradox establishes that efficiency improvements increase total consumption by enabling expanded usage. GPT-3's launch at $0.06 per 1,000 tokens enabled applications impossible at GPT-2's economics; reducing costs to $0.002 per 1,000 tokens (30$\times$ improvement) triggered a 100$\times$ increase in query volume, growing total emissions despite per-query efficiency gains. Quantization that reduces inference energy by 4$\times$ often leads to 10$\times$ deployment expansion as cost constraints relax. Organizations that optimize efficiency without usage governance consistently experience 3-5$\times$ consumption growth within six months of deployment, transforming sustainability wins into consumption explosions requiring carbon budgets and usage caps as discussed in @sec-sustainable-ai-carbonaware-scheduling-c2db. +Engineers assume that halving inference cost cuts environmental impact in half. In production, Jevons Paradox establishes that efficiency improvements increase total consumption by enabling expanded usage. GPT-3's launch at $0.06 per 1,000 tokens enabled applications impossible at GPT-2's economics; reducing costs to $0.002 per 1,000 tokens (30 $\times$ improvement) triggered a 100 $\times$ increase in query volume, growing total emissions despite per-query efficiency gains. Quantization that reduces inference energy by 4 $\times$ often leads to 10 $\times$ deployment expansion as cost constraints relax. Organizations that optimize efficiency without usage governance consistently experience 3-5 $\times$ consumption growth within six months of deployment, transforming sustainability wins into consumption explosions requiring carbon budgets and usage caps as discussed in @sec-sustainable-ai-carbonaware-scheduling-c2db. **Pitfall:** ***Treating carbon offsets as a substitute for reducing actual emissions.*** -Organizations purchase offsets to neutralize emissions without validating offset quality. In reality, analysis of voluntary carbon markets reveals that 60-90% of credits fail to deliver claimed reductions due to inflated baselines, non-permanent sequestration, or projects that would have occurred regardless. A company training models on coal grids (1000 g CO₂/kWh) and buying offsets spends 2-3$\times$ more than directly migrating to renewable regions (20-50 g CO₂/kWh) while achieving inferior environmental outcomes. Offset projects take 5-20 years to sequester carbon while compute emissions are immediate. Teams that prioritize offsets over actual reduction miss the 20-50$\times$ leverage available through geographic optimization shown in @sec-sustainable-ai-carbon-footprint-calculation-c2fe and delay renewable energy transitions that deliver permanent improvements. +Organizations purchase offsets to neutralize emissions without validating offset quality. In reality, analysis of voluntary carbon markets reveals that 60-90% of credits fail to deliver claimed reductions due to inflated baselines, non-permanent sequestration, or projects that would have occurred regardless. A company training models on coal grids (1000 g CO₂/kWh) and buying offsets spends 2-3 $\times$ more than directly migrating to renewable regions (20-50 g CO₂/kWh) while achieving inferior environmental outcomes. Offset projects take 5-20 years to sequester carbon while compute emissions are immediate. Teams that prioritize offsets over actual reduction miss the 20-50 $\times$ leverage available through geographic optimization shown in @sec-sustainable-ai-carbon-footprint-calculation-c2fe and delay renewable energy transitions that deliver permanent improvements. **Pitfall:** ***Optimizing individual components without analyzing system-level lifecycle impacts.*** -Teams reduce training cost to improve sustainability without analyzing deployment scale. In production, training-inference trade-offs often invert total emissions. A model pruned by 40% to save training energy but requiring 2$\times$ inference compute increases total lifecycle emissions if it serves more than 100 million queries—a crossover point reached in 3-6 months for production systems. Edge deployment that reduces datacenter energy by 60% but requires manufacturing 10,000 specialized devices adds 1,500-2,000 kg embodied carbon (10$\times$ the cloud training emissions). Extending GPU lifetime from 3 to 5 years reduces amortized embodied carbon by 40% but may sacrifice 15-25% operational efficiency; the lifecycle break-even depends on grid carbon intensity, with lifetime extension dominating on clean grids and efficiency winning on dirty grids. Effective sustainability requires holistic analysis across @sec-sustainable-ai-lifecycle-carbon-accounting-99a1 rather than local optimization. +Teams reduce training cost to improve sustainability without analyzing deployment scale. In production, training-inference trade-offs often invert total emissions. A model pruned by 40% to save training energy but requiring 2 $\times$ inference compute increases total lifecycle emissions if it serves more than 100 million queries—a crossover point reached in 3-6 months for production systems. Edge deployment that reduces datacenter energy by 60% but requires manufacturing 10,000 specialized devices adds 1,500-2,000 kg embodied carbon (10 $\times$ the cloud training emissions). Extending GPU lifetime from 3 to 5 years reduces amortized embodied carbon by 40% but may sacrifice 15-25% operational efficiency; the lifecycle break-even depends on grid carbon intensity, with lifetime extension dominating on clean grids and efficiency winning on dirty grids. Effective sustainability requires holistic analysis across @sec-sustainable-ai-lifecycle-carbon-accounting-99a1 rather than local optimization. ## Summary {#sec-sustainable-ai-summary-8cec} @@ -2912,11 +2912,11 @@ We established that sustainability is not a "nice-to-have" but a core engineerin ::: {.callout-takeaways} -* **The Sustainability Paradox**: AI compute demands are growing 10$\times$ faster than hardware efficiency gains. Without algorithmic intervention (e.g., pruning, quantization), the Machine Learning Fleet will hit a "Power Wall" that constrains all future innovation. +* **The Sustainability Paradox**: AI compute demands are growing 10 $\times$ faster than hardware efficiency gains. Without algorithmic intervention (e.g., pruning, quantization), the Machine Learning Fleet will hit a "Power Wall" that constrains all future innovation. * **The Inefficiency of Decode**: Autoregressive token generation is notoriously energy-wasteful. While "Prefill" is compute-bound and efficient, "Decode" is bandwidth-bound, leaving GPUs idling and drawing massive static power. Specialized, memory-optimized NPUs/TPUs are essential for sustainable serving. * **Embodied Carbon is Real**: Up to 30% of a system's lifecycle emissions occur before it is ever powered on. The manufacturing of sub-5nm chips is water- and chemical-intensive, making hardware longevity and circular economy reuse critical MLOps concerns. * **Jevons Paradox**: Improving the efficiency of AI tokens reduces their cost, which often triggers a massive increase in total demand. Sustainable AI requires a dual strategy: technical optimization combined with carbon-aware governance. -* **Carbon-Aware Scheduling**: Geographic placement is the highest-leverage sustainability choice. Moving a training job from a coal-powered grid to a hydro-powered one can reduce emissions by 20–50$\times$ without changing a single line of code. +* **Carbon-Aware Scheduling**: Geographic placement is the highest-leverage sustainability choice. Moving a training job from a coal-powered grid to a hydro-powered one can reduce emissions by 20–50 $\times$ without changing a single line of code. ::: Sustainability, security, and robustness complete our framework for **Production AI**. We now have a fleet that is powerful, secure, resilient, and efficient. But a machine that is technically perfect can still be socially destructive. diff --git a/book/quarto/scripts/mit_press/FIGURE_LIST_VOL1.csv b/book/quarto/scripts/mit_press/FIGURE_LIST_VOL1.csv index 74f9f1b25f..02679633d0 100644 --- a/book/quarto/scripts/mit_press/FIGURE_LIST_VOL1.csv +++ b/book/quarto/scripts/mit_press/FIGURE_LIST_VOL1.csv @@ -97,7 +97,7 @@ Chapter,Figure Number,Label,Caption,Alt-Text "Training","8.5","fig-training-loop","**Single-GPU Training Loop**: The three sequential steps of one training iteration: the forward pass generates predictions, gradient computation propagates error signals backward, and the optimizer applies parameter updates. GPUs parallelize the underlying matrix operations, accelerating both the forward and backward passes.","Neural network diagram showing data cylinders feeding into a network of connected nodes. A GPU box at bottom processes the forward and backward pass computations." "Training","8.6","fig-data-pipeline","**CPU-to-GPU Data Flow**: Three distinct zones compose the data pipeline: the storage zone houses raw data on disk, the CPU preprocessing zone handles format conversion, processing, and batching, and the GPU training zone distributes preprocessed batches across multiple GPU workers for parallel computation.","Block diagram showing data flow through three zones: Storage Zone with raw data, CPU Preprocessing Zone with format, process, and batch stages, and GPU Training Zone with three GPU workers." "Training","8.7","fig-galore-llm-memory-breakdown","**Memory Footprint Breakdown**: Memory usage of LLaMA-7B across four optimizer configurations, decomposed into weights, activations, optimizer state, weight gradients, and other components. The dashed red line marks the RTX 4090 24 GB memory limit, illustrating how standard Adam exceeds single-GPU capacity while GaLoRE compression reduces optimizer state enough to fit within this budget.","Stacked horizontal bar chart comparing memory usage across four optimizers for LLaMA-7B. Shows components: others, weight gradient, optimization, activation, and weight. Dashed red line marks RTX 4090 memory limit at 24 GB." -"Training","8.8","fig-linear-scaling-failure","The Linear Scaling Failure. Training Loss vs. Steps. Curve A (Blue) represents a standard baseline batch size. Curve B (Gray) shows what happens when batch size is increased 8x without tuning: convergence slows dramatically because weight updates are too infrequent. Curve C (Green) restores convergence by scaling the learning rate linearly (8x LR), allowing the model to take larger steps to compensate for fewer updates.","Line chart of Loss vs Steps. Blue line (Baseline) converges fast. Gray line (Large Batch Naive) converges slow. Green line (Scaled LR) matches the baseline." +"Training","8.8","fig-linear-scaling-failure","The Linear Scaling Failure. Training Loss vs. Epochs. Curve A (Blue) represents a standard baseline batch size. Curve B (Gray) shows what happens when batch size is increased 8x without tuning: convergence slows dramatically because weight updates are too infrequent (per epoch). Curve C (Green) restores convergence by scaling the learning rate linearly (8x LR), allowing the model to take larger steps to compensate for fewer updates.","Line chart of Loss vs Epochs. Blue line (Baseline) converges fast. Gray line (Large Batch Naive) converges slow. Green line (Scaled LR) matches the baseline." "Training","8.9","fig-tf-bottleneck-trace","Data-Bound Profiler Trace: TensorFlow profiler output capturing a data loading bottleneck during training. The gaps in GPU activity (white regions between compute blocks) indicate periods where the device idles while waiting for input data, with utilization dropping to zero during data loading phases.","TensorFlow profiler screenshot showing GPU activity timeline. Colored blocks indicate computation periods with white gaps revealing idle time when GPU waits for data loading to complete." "Training","8.10","fig-optimization-flowchart","**Training Optimization Decision Flowchart**: Systematic approach to optimization selection based on profiling results. Begin by measuring GPU utilization, then follow the decision path to identify whether the bottleneck is data-bound, memory-bound, or compute-bound. Each path leads to specific techniques that address the identified constraint.","Flowchart showing optimization decision tree starting from Profile Training Run, branching based on GPU utilization and memory pressure to different optimization techniques." "Training","8.11","fig-fetching-naive","**Sequential Data Fetching**: File open, read, and train operations execute serially across two epochs, with the GPU remaining idle during all file operations. The full sequential pipeline spans approximately 90 seconds, establishing the baseline that overlapped prefetching improves upon.","Gantt chart showing sequential data pipeline over two epochs. Four rows: Open, Read, Train, and Epoch. Operations execute serially with gaps between phases, spanning from 00:00 to 01:30." diff --git a/book/quarto/tex/header-includes.tex b/book/quarto/tex/header-includes.tex index 578acc409b..a4e1febe1f 100644 --- a/book/quarto/tex/header-includes.tex +++ b/book/quarto/tex/header-includes.tex @@ -1326,15 +1326,13 @@ align=right,font={\fontsize{40pt}{40}\selectfont}] % 1-99: Gradient shading (contextual relevance) % % Argument 8: Data flow intensity (0 to 100) -% Reserved for future use. The data bar's gradient is now derived -% entirely from arguments 1-7. Kept for interface stability. -% -% Data bar (smooth gradient): -% The data bar's color at each layer's height matches that layer's -% intensity. PGF interpolates smoothly between stops, so the gradient -% flows from one layer to the next — like color filling a pipe between -% inlets. Two distant bright layers produce a gradient that stays warm -% across the space between them, making "skip connections" visible. +% Controls the uniform color of the data bar: +% - data=0: Gray (inactive — data is not a focus) +% - data=90: Strong crimson (data is the dominant concern) +% - data=30: Moderate tint (data is contextually relevant) +% The data bar is always a single uniform color, never a gradient. +% This communicates a clear, orthogonal message: "how much does data +% matter in this chapter?" — independent of which stack layers are lit. % % Data connectors ("wires"): % Each layer has a horizontal connector line from its right edge to the @@ -1342,11 +1340,11 @@ align=right,font={\fontsize{40pt}{40}\selectfont}] % layer is crimson at 60%, its wire to the data bar is also crimson at % 60%. If a layer is gray (0%), its wire is faint gray. % -% This reveals data's role as a cross-cutting concern: data is the shared -% medium that lets non-adjacent layers influence each other. When two -% distant layers are both strongly colored, their bright wires into the -% data bar make the "skip connection" visually obvious — they're linked -% through data even though the layers between them are gray. +% The wires show how strongly each layer plugs into data. A bright wire +% from Training into a bright data bar says "training depends heavily on +% data in this chapter." A faint wire from Hardware into a bright data +% bar says "hardware doesn't interact with data here, but data still +% dominates the chapter." % % Stack (bottom to top): % 1. Hardware — Accelerators, silicon, benchmarking @@ -1488,68 +1486,24 @@ align=right,font={\fontsize{40pt}{40}\selectfont}] % Layer 1: Hardware \stacklayer{0.0}{Hardware}{#1}{icon_hardware} - % --- Data: vertical bar with smooth gradient --- - % The data bar acts as a circuit bus. Its shading flows smoothly - % between the layer intensities: bright where strongly-connected - % layers plug in, dim where inactive layers sit. This makes the - % "current path" through the bus visible — two distant bright - % layers light up the bus between them, closing the circuit. - % - % Color stops are placed at each layer's midpoint (where the wire - % meets the bar), using that layer's crimson intensity. PGF - % interpolates smoothly between stops. - % - % The bar height is 7.20cm (-0.10 to 7.10). Color stop positions - % are expressed as percentages of the 100bp shading height. - % HW midpoint=0.5 → (0.5+0.10)/7.20 ≈ 8.3% → 8bp - % FW midpoint=1.5 → (1.5+0.10)/7.20 ≈ 22.2% → 22bp - % MOD midpoint=2.5 → (2.5+0.10)/7.20 ≈ 36.1% → 36bp - % TRN midpoint=3.5 → (3.5+0.10)/7.20 ≈ 50.0% → 50bp - % SRV midpoint=4.5 → (4.5+0.10)/7.20 ≈ 63.9% → 64bp - % OPS midpoint=5.5 → (5.5+0.10)/7.20 ≈ 77.8% → 78bp - % APP midpoint=6.5 → (6.5+0.10)/7.20 ≈ 91.7% → 92bp + % --- Data: vertical bar with uniform fill --- + % The data bar is a solid color block whose intensity is set by + % arg 8. It communicates an orthogonal message from the layer + % boxes: "how central is data to this chapter?" The wires still + % connect each layer to the bar at that layer's own intensity, + % showing which layers interact with data. - % Data bar gradient: color stops = layer intensities. - % The wires are inlets into the bus. The data bar color at each - % layer's height matches that wire's intensity. PGF interpolates - % smoothly between stops, so the gradient flows naturally from - % one wire to the next — like color filling a pipe between two - % inlets. Two distant bright wires produce a gradient that stays - % warm across the space between them. - \pgfmathtruncatemacro{\dhw}{#1} - \pgfmathtruncatemacro{\dfw}{#2} - \pgfmathtruncatemacro{\dmod}{#3} - \pgfmathtruncatemacro{\dtrn}{#4} - \pgfmathtruncatemacro{\dsrv}{#5} - \pgfmathtruncatemacro{\dops}{#6} - \pgfmathtruncatemacro{\dapp}{#7} + % Data bar: uniform fill from arg 8. + % The bar is a single solid color — no gradient. This gives a clear + % reading: "data matters this much in this chapter." + \pgfmathtruncatemacro{\ddata}{#8} + \ifnum\ddata=0 + \def\databarcol{black!5} + \else + \def\databarcol{crimson!\ddata} + \fi - % Define stop colors by mixing crimson with white. - % crimson!N!white means N% crimson + (100-N)% white. - % At 0% intensity this produces pure white (faint but continuous). - \colorlet{stop@hw}{crimson!\dhw!white} - \colorlet{stop@fw}{crimson!\dfw!white} - \colorlet{stop@mod}{crimson!\dmod!white} - \colorlet{stop@trn}{crimson!\dtrn!white} - \colorlet{stop@srv}{crimson!\dsrv!white} - \colorlet{stop@ops}{crimson!\dops!white} - \colorlet{stop@app}{crimson!\dapp!white} - - % Declare the smooth vertical shading with 7 color stops - \pgfdeclareverticalshading{databarshading}{100bp}{% - color(0bp)=(stop@hw); - color(8bp)=(stop@hw); - color(22bp)=(stop@fw); - color(36bp)=(stop@mod); - color(50bp)=(stop@trn); - color(64bp)=(stop@srv); - color(78bp)=(stop@ops); - color(92bp)=(stop@app); - color(100bp)=(stop@app) - } - - % Draw the data bar with the gradient shading - \shade[shading=databarshading, rounded corners=1.5pt] + \fill[\databarcol, rounded corners=1.5pt] (2.85, -0.1) rectangle (3.15, 7.10); % Horizontal connectors ("wires") from each layer into the data bar. @@ -1569,12 +1523,11 @@ align=right,font={\fontsize{40pt}{40}\selectfont}] \draw[\wirecol, line width=1.2pt] (2.6, \ypos) -- (2.85, \ypos); } - % "Data" label — color based on max layer intensity for readability - \pgfmathtruncatemacro{\maxint}{max(max(max(#1,#2),max(#3,#4)),max(max(#5,#6),#7))} - \ifnum\maxint>50 + % "Data" label — color based on data bar intensity for readability + \ifnum\ddata>50 \def\datatextcol{white} \else - \ifnum\maxint<10 + \ifnum\ddata<10 \def\datatextcol{black!25} \else \def\datatextcol{crimson} @@ -1656,41 +1609,17 @@ align=right,font={\fontsize{40pt}{40}\selectfont}] % Layer 1: Hardware \stacklayerfull{0.0}{Hardware}{GPUs, TPUs, accelerators}{#1}{icon_hardware} - % --- Data: smooth gradient bar (same logic as \mlsysstack) --- - % Bar height: 8.00cm (-0.15 to 7.85). Layer midpoints for full-size: - % HW=0.55, FW=1.65, MOD=2.75, TRN=3.85, SRV=4.95, OPS=6.05, APP=7.15 - % As % of height: 8.8%, 22.5%, 36.3%, 50.0%, 63.8%, 77.5%, 91.3% + % --- Data: uniform color bar (same logic as \mlsysstack) --- - % Data bar gradient: stops = layer intensities (same as \mlsysstack) - \pgfmathtruncatemacro{\dhw}{#1} - \pgfmathtruncatemacro{\dfw}{#2} - \pgfmathtruncatemacro{\dmod}{#3} - \pgfmathtruncatemacro{\dtrn}{#4} - \pgfmathtruncatemacro{\dsrv}{#5} - \pgfmathtruncatemacro{\dops}{#6} - \pgfmathtruncatemacro{\dapp}{#7} + % Data bar: uniform fill from arg 8 (same logic as \mlsysstack) + \pgfmathtruncatemacro{\ddata}{#8} + \ifnum\ddata=0 + \def\databarcol{black!5} + \else + \def\databarcol{crimson!\ddata} + \fi - \colorlet{stopf@hw}{crimson!\dhw!white} - \colorlet{stopf@fw}{crimson!\dfw!white} - \colorlet{stopf@mod}{crimson!\dmod!white} - \colorlet{stopf@trn}{crimson!\dtrn!white} - \colorlet{stopf@srv}{crimson!\dsrv!white} - \colorlet{stopf@ops}{crimson!\dops!white} - \colorlet{stopf@app}{crimson!\dapp!white} - - \pgfdeclareverticalshading{databarshadingfull}{100bp}{% - color(0bp)=(stopf@hw); - color(9bp)=(stopf@hw); - color(22bp)=(stopf@fw); - color(36bp)=(stopf@mod); - color(50bp)=(stopf@trn); - color(64bp)=(stopf@srv); - color(78bp)=(stopf@ops); - color(91bp)=(stopf@app); - color(100bp)=(stopf@app) - } - - \shade[shading=databarshadingfull, rounded corners=2pt] + \fill[\databarcol, rounded corners=2pt] (4.45, -0.15) rectangle (4.75, 7.85); % Horizontal connectors ("wires") — each colored by its layer's intensity @@ -1704,12 +1633,11 @@ align=right,font={\fontsize{40pt}{40}\selectfont}] \draw[\wirecol, line width=1.5pt] (4.0, \ypos) -- (4.45, \ypos); } - % "Data" label - \pgfmathtruncatemacro{\maxint}{max(max(max(#1,#2),max(#3,#4)),max(max(#5,#6),#7))} - \ifnum\maxint>50 + % "Data" label — color based on data bar intensity for readability + \ifnum\ddata>50 \def\datatextcol{white} \else - \ifnum\maxint<10 + \ifnum\ddata<10 \def\datatextcol{black!25} \else \def\datatextcol{crimson} diff --git a/book/tools/scripts/gen_bio_nn_svg.py b/book/tools/scripts/gen_bio_nn_svg.py new file mode 100644 index 0000000000..1d107bd2c8 --- /dev/null +++ b/book/tools/scripts/gen_bio_nn_svg.py @@ -0,0 +1,188 @@ + +import math + +def generate_svg(): + width = 900 + height = 380 + + svg_header = f''' + + + + + + + + + + + + + + + + + + + Biological Neuron + Artificial Neuron + +''' + + # --- BIOLOGICAL NEURON (Improved) --- + # Soma (Cell Body) - more organic shape + bio_soma = ''' + + + ''' + + # Dendrites - more branching + # Main branches radiating from Soma + dendrites = ''' + + + + + + + + + + + + + + + ''' + + # Axon - clearer segmentation + axon = ''' + + + + + + + + + + + + + + + ''' + + # Labels for Bio + bio_labels = ''' + Dendrites + Cell Body (Soma) + Axon + Synapses + ''' + + # --- ARTIFICIAL NEURON --- + # Inputs + art_inputs = ''' + + + x₀ + =1 + + + x₁ + + + x₂ + + ... + + + xₙ + + Inputs + ''' + + # Processing Unit + art_proc = ''' + + + + + + + + + w0 + w1 + w2 + wn + + + + + z + f + + + Sum + Activation + + + + y + Output + + ''' + + # --- CONNECTIONS (Simplified) --- + # Cleaner, dashed lines without text boxes to reduce clutter + connections = ''' + + + + + + + + + + + + + + + + + ''' + + svg_footer = '' + + return svg_header + bio_soma + dendrites + axon + bio_labels + art_inputs + art_proc + connections + svg_footer + +with open('book/quarto/contents/vol1/nn_computation/images/svg/bio_nn2ai_nn_improved.svg', 'w') as f: + f.write(generate_svg()) diff --git a/book/tools/scripts/genai/generate_comm_primitives.py b/book/tools/scripts/genai/generate_comm_primitives.py new file mode 100644 index 0000000000..3fa6ca909b --- /dev/null +++ b/book/tools/scripts/genai/generate_comm_primitives.py @@ -0,0 +1,153 @@ + +import sys +import os +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import numpy as np + +# Add book/quarto/mlsys to path to import viz +# Script is in book/tools/scripts/genai/ +# We need to reach book/quarto/mlsys +# ../../../quarto/mlsys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../quarto/mlsys"))) + +try: + import viz + viz.set_book_style() + COLORS = viz.COLORS +except ImportError: + print("Warning: Could not import viz.py, using fallback style.") + COLORS = { + "primary": "#333333", + "RedLine": "#CB202D", + "BlueLine": "#006395", + "GreenLine": "#008F45", + "OrangeLine": "#E67817", + "grid": "#CCCCCC" + } + plt.style.use('seaborn-v0_8-whitegrid') + +# Custom Yellow that fits the palette better than pure yellow but is distinct from Orange +COLORS["YellowLine"] = "#F4D03F" + +def draw_node(ax, x, y, label=None, radius=0.15, color="#E0E0E0"): + """Draws a circular node.""" + circle = patches.Circle((x, y), radius, facecolor=color, edgecolor=COLORS["primary"], linewidth=1.5, zorder=10) + ax.add_patch(circle) + if label: + ax.text(x, y, label, ha='center', va='center', fontsize=14, fontweight='bold', zorder=11, color=COLORS["primary"]) + return circle + +def draw_rect(ax, x, y, width, height, color): + """Draws a data rectangle centered at (x, y).""" + rect = patches.Rectangle((x - width/2, y - height/2), width, height, + facecolor=color, edgecolor=None, zorder=15) + ax.add_patch(rect) + +def draw_arrow(ax, start, end): + """Draws an arrow from start to end.""" + ax.annotate("", xy=end, xytext=start, + arrowprops=dict(arrowstyle="-|>", color=COLORS["primary"], lw=1.5, shrinkA=0, shrinkB=0), + zorder=5) + +def setup_subplot(ax, title): + ax.set_xlim(-1.2, 1.2) + ax.set_ylim(-0.2, 1.8) + ax.axis('off') + # Title at the bottom + ax.text(0, -0.15, title, ha='center', va='center', fontsize=14, fontweight='bold', color=COLORS["primary"]) + +def generate_diagram(): + fig, axes = plt.subplots(2, 2, figsize=(10, 8)) + + # Node positions + src_y = 0.2 + dest_y = 1.4 + + # Horizontal spacing for 4 nodes + xs = np.linspace(-0.9, 0.9, 4) + + # Colors + c_red = COLORS["RedLine"] + c_yellow = COLORS["YellowLine"] + c_green = COLORS["GreenLine"] + c_blue = COLORS["BlueLine"] + palette = [c_red, c_yellow, c_green, c_blue] + + # --- 1. Broadcast (Top Left) --- + ax = axes[0, 0] + setup_subplot(ax, "Broadcast") + + # Source (Bottom) + draw_node(ax, 0, src_y) + draw_rect(ax, 0, src_y, 0.08, 0.15, c_red) + + # Dests (Top) + for x in xs: + draw_node(ax, x, dest_y) + draw_rect(ax, x, dest_y, 0.08, 0.15, c_red) + # Arrow + draw_arrow(ax, (0, src_y + 0.15), (x, dest_y - 0.15)) + + # --- 2. Scatter (Top Right) --- + ax = axes[0, 1] + setup_subplot(ax, "Scatter") + + # Source (Bottom) + draw_node(ax, 0, src_y) + # Composite block at source + w = 0.06 + h = 0.12 + total_w = 4 * w + start_x = -total_w / 2 + w/2 + for i, color in enumerate(palette): + draw_rect(ax, start_x + i*w, src_y, w, h, color) + + # Dests (Top) + for i, x in enumerate(xs): + draw_node(ax, x, dest_y) + draw_rect(ax, x, dest_y, 0.08, 0.15, palette[i]) + # Arrow + draw_arrow(ax, (0, src_y + 0.15), (x, dest_y - 0.15)) + + # --- 3. Gather (Bottom Left) --- + ax = axes[1, 0] + setup_subplot(ax, "Gather") + + # Dest (Bottom) + draw_node(ax, 0, src_y) + # Composite block at dest + start_x = -total_w / 2 + w/2 + for i, color in enumerate(palette): + draw_rect(ax, start_x + i*w, src_y, w, h, color) + + # Sources (Top) + for i, x in enumerate(xs): + draw_node(ax, x, dest_y) + draw_rect(ax, x, dest_y, 0.08, 0.15, palette[i]) + # Arrow + draw_arrow(ax, (x, dest_y - 0.15), (0, src_y + 0.15)) + + # --- 4. Reduction (Bottom Right) --- + ax = axes[1, 1] + setup_subplot(ax, "Reduction") + + # Dest (Bottom) + draw_node(ax, 0, src_y, label="16") + + # Sources (Top) + values = ["1", "3", "5", "7"] + for i, x in enumerate(xs): + draw_node(ax, x, dest_y, label=values[i]) + # Arrow + draw_arrow(ax, (x, dest_y - 0.15), (0, src_y + 0.15)) + + plt.tight_layout() + + # Save + output_path = os.path.abspath("comm_primitives.png") + plt.savefig(output_path, dpi=300, bbox_inches='tight') + print(f"Generated image at: {output_path}") + +if __name__ == "__main__": + generate_diagram() diff --git a/book/tools/scripts/genai/generate_comm_primitives_diagram.py b/book/tools/scripts/genai/generate_comm_primitives_diagram.py new file mode 100644 index 0000000000..3fa6ca909b --- /dev/null +++ b/book/tools/scripts/genai/generate_comm_primitives_diagram.py @@ -0,0 +1,153 @@ + +import sys +import os +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import numpy as np + +# Add book/quarto/mlsys to path to import viz +# Script is in book/tools/scripts/genai/ +# We need to reach book/quarto/mlsys +# ../../../quarto/mlsys +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../quarto/mlsys"))) + +try: + import viz + viz.set_book_style() + COLORS = viz.COLORS +except ImportError: + print("Warning: Could not import viz.py, using fallback style.") + COLORS = { + "primary": "#333333", + "RedLine": "#CB202D", + "BlueLine": "#006395", + "GreenLine": "#008F45", + "OrangeLine": "#E67817", + "grid": "#CCCCCC" + } + plt.style.use('seaborn-v0_8-whitegrid') + +# Custom Yellow that fits the palette better than pure yellow but is distinct from Orange +COLORS["YellowLine"] = "#F4D03F" + +def draw_node(ax, x, y, label=None, radius=0.15, color="#E0E0E0"): + """Draws a circular node.""" + circle = patches.Circle((x, y), radius, facecolor=color, edgecolor=COLORS["primary"], linewidth=1.5, zorder=10) + ax.add_patch(circle) + if label: + ax.text(x, y, label, ha='center', va='center', fontsize=14, fontweight='bold', zorder=11, color=COLORS["primary"]) + return circle + +def draw_rect(ax, x, y, width, height, color): + """Draws a data rectangle centered at (x, y).""" + rect = patches.Rectangle((x - width/2, y - height/2), width, height, + facecolor=color, edgecolor=None, zorder=15) + ax.add_patch(rect) + +def draw_arrow(ax, start, end): + """Draws an arrow from start to end.""" + ax.annotate("", xy=end, xytext=start, + arrowprops=dict(arrowstyle="-|>", color=COLORS["primary"], lw=1.5, shrinkA=0, shrinkB=0), + zorder=5) + +def setup_subplot(ax, title): + ax.set_xlim(-1.2, 1.2) + ax.set_ylim(-0.2, 1.8) + ax.axis('off') + # Title at the bottom + ax.text(0, -0.15, title, ha='center', va='center', fontsize=14, fontweight='bold', color=COLORS["primary"]) + +def generate_diagram(): + fig, axes = plt.subplots(2, 2, figsize=(10, 8)) + + # Node positions + src_y = 0.2 + dest_y = 1.4 + + # Horizontal spacing for 4 nodes + xs = np.linspace(-0.9, 0.9, 4) + + # Colors + c_red = COLORS["RedLine"] + c_yellow = COLORS["YellowLine"] + c_green = COLORS["GreenLine"] + c_blue = COLORS["BlueLine"] + palette = [c_red, c_yellow, c_green, c_blue] + + # --- 1. Broadcast (Top Left) --- + ax = axes[0, 0] + setup_subplot(ax, "Broadcast") + + # Source (Bottom) + draw_node(ax, 0, src_y) + draw_rect(ax, 0, src_y, 0.08, 0.15, c_red) + + # Dests (Top) + for x in xs: + draw_node(ax, x, dest_y) + draw_rect(ax, x, dest_y, 0.08, 0.15, c_red) + # Arrow + draw_arrow(ax, (0, src_y + 0.15), (x, dest_y - 0.15)) + + # --- 2. Scatter (Top Right) --- + ax = axes[0, 1] + setup_subplot(ax, "Scatter") + + # Source (Bottom) + draw_node(ax, 0, src_y) + # Composite block at source + w = 0.06 + h = 0.12 + total_w = 4 * w + start_x = -total_w / 2 + w/2 + for i, color in enumerate(palette): + draw_rect(ax, start_x + i*w, src_y, w, h, color) + + # Dests (Top) + for i, x in enumerate(xs): + draw_node(ax, x, dest_y) + draw_rect(ax, x, dest_y, 0.08, 0.15, palette[i]) + # Arrow + draw_arrow(ax, (0, src_y + 0.15), (x, dest_y - 0.15)) + + # --- 3. Gather (Bottom Left) --- + ax = axes[1, 0] + setup_subplot(ax, "Gather") + + # Dest (Bottom) + draw_node(ax, 0, src_y) + # Composite block at dest + start_x = -total_w / 2 + w/2 + for i, color in enumerate(palette): + draw_rect(ax, start_x + i*w, src_y, w, h, color) + + # Sources (Top) + for i, x in enumerate(xs): + draw_node(ax, x, dest_y) + draw_rect(ax, x, dest_y, 0.08, 0.15, palette[i]) + # Arrow + draw_arrow(ax, (x, dest_y - 0.15), (0, src_y + 0.15)) + + # --- 4. Reduction (Bottom Right) --- + ax = axes[1, 1] + setup_subplot(ax, "Reduction") + + # Dest (Bottom) + draw_node(ax, 0, src_y, label="16") + + # Sources (Top) + values = ["1", "3", "5", "7"] + for i, x in enumerate(xs): + draw_node(ax, x, dest_y, label=values[i]) + # Arrow + draw_arrow(ax, (x, dest_y - 0.15), (0, src_y + 0.15)) + + plt.tight_layout() + + # Save + output_path = os.path.abspath("comm_primitives.png") + plt.savefig(output_path, dpi=300, bbox_inches='tight') + print(f"Generated image at: {output_path}") + +if __name__ == "__main__": + generate_diagram() diff --git a/book/tools/scripts/genai/replace_tikz.py b/book/tools/scripts/genai/replace_tikz.py new file mode 100644 index 0000000000..387f9a0e77 --- /dev/null +++ b/book/tools/scripts/genai/replace_tikz.py @@ -0,0 +1,45 @@ + +import os + +filepath = "book/quarto/contents/vol1/nn_architectures/nn_architectures.qmd" + +with open(filepath, "r") as f: + lines = f.readlines() + +new_lines = [] +in_fig_div = False +in_tikz_block = False +replaced = False + +for line in lines: + if "::: {#fig-collective-comm" in line: + in_fig_div = True + new_lines.append(line) + continue + + if in_fig_div: + if "```{.tikz}" in line: + in_tikz_block = True + if not replaced: + new_lines.append("![](images/png/comm_primitives.png)\n") + replaced = True + continue + + if in_tikz_block: + if "```" in line.strip() and line.strip() == "```": + in_tikz_block = False + continue + + if line.strip() == ":::": + in_fig_div = False + new_lines.append(line) + continue + + new_lines.append(line) + else: + new_lines.append(line) + +with open(filepath, "w") as f: + f.writelines(new_lines) + +print("Replaced TikZ block with image reference.") diff --git a/book/vscode-ext/README.md b/book/vscode-ext/README.md index 7305e4e8a8..f036173549 100644 --- a/book/vscode-ext/README.md +++ b/book/vscode-ext/README.md @@ -2,6 +2,23 @@ The MLSysBook extension provides editor-first workflows for building, debugging, and validating the book without leaving VS Code. +## Loading the extension (so you see the latest code) + +The editor runs **one** copy of the extension. Reload only reloads that copy. If that copy isn’t from this repo, you won’t see new changes (e.g. Reset Quarto config) until you point the editor at this repo’s extension. + +**Option A – Run from repo (recommended when developing)** +1. Open the **repo root** (`mlsysbook-vols`) in Cursor/VS Code. +2. Run **`npm run compile`** in `book/vscode-ext` (or run it from the repo root: `cd book/vscode-ext && npm run compile`). +3. Press **F5** (or Run → Start Debugging → “Run MLSysBook Extension”). +4. A **new window** opens; that window is running the extension from `book/vscode-ext`. Use that window for the book (Reset, Build, etc.). +5. After code changes: run **compile** again, then in that new window use **Developer: Reload Window** so it picks up the new code. + +**Option B – Install from this folder** +1. Command Palette → **Developer: Install Extension from Location…** +2. Choose the folder: **`mlsysbook-vols/book/vscode-ext`**. +3. Reload the window when prompted. +4. After code changes: run **`npm run compile`** in `book/vscode-ext`, then **Developer: Reload Window**. No need to install again unless you remove the extension. + ## Binder-First Integration This extension treats `./book/binder` as the operational backend. diff --git a/book/vscode-ext/package.json b/book/vscode-ext/package.json index 32a65de762..2638f93660 100644 --- a/book/vscode-ext/package.json +++ b/book/vscode-ext/package.json @@ -71,14 +71,14 @@ { "command": "mlsysbook.buildVolumeHtml", "title": "MLSysBook: Build Full Volume (HTML)" }, { "command": "mlsysbook.buildVolumePdf", "title": "MLSysBook: Build Full Volume (PDF)" }, { "command": "mlsysbook.buildVolumeEpub", "title": "MLSysBook: Build Full Volume (EPUB)" }, - { "command": "mlsysbook.debugVolumePdf", "title": "MLSysBook: Debug Volume PDF" }, - { "command": "mlsysbook.debugChapterSections", "title": "MLSysBook: Debug Chapter Sections" }, - { "command": "mlsysbook.debugParallelChapters", "title": "MLSysBook: Parallel Debug Chapters" }, - { "command": "mlsysbook.debugBisectChapters", "title": "MLSysBook: Bisect Failing Chapters" }, + { "command": "mlsysbook.buildFullVolume", "title": "MLSysBook: Build Full Volume..." }, + { "command": "mlsysbook.buildSelectedChapters", "title": "MLSysBook: Build Chapters..." }, + { "command": "mlsysbook.buildAllChaptersParallel", "title": "MLSysBook: Build All Chapters (Parallel)..." }, + { "command": "mlsysbook.resetQuartoConfig", "title": "Reset Quarto Config" }, + { "command": "mlsysbook.testAllChaptersParallel", "title": "MLSysBook: Test All Chapters (Parallel)" }, { "command": "mlsysbook.cancelParallelSession", "title": "MLSysBook: Cancel Current Parallel Session" }, { "command": "mlsysbook.rerunFailedParallel", "title": "MLSysBook: Rerun Failed Chapters" }, { "command": "mlsysbook.quickBuildCurrentChapterPdf", "title": "MLSysBook: Quick Build Current Chapter (PDF)" }, - { "command": "mlsysbook.quickParallelDebugSelectedPdf", "title": "MLSysBook: Quick Parallel Debug Selected Chapters (PDF)" }, { "command": "mlsysbook.renameLabelReferences", "title": "MLSysBook: Rename Label and References" }, { "command": "mlsysbook.addSectionIds", "title": "MLSysBook: Add Missing Section IDs" }, { "command": "mlsysbook.verifySectionIds", "title": "MLSysBook: Verify Section IDs" }, @@ -114,9 +114,9 @@ { "command": "mlsysbook.revealTerminal", "title": "MLSysBook: Reveal Build Terminal", "icon": "$(terminal)" }, { "command": "mlsysbook.historyRerunCommand", "title": "MLSysBook: Rerun Command (Recent Runs)" }, { "command": "mlsysbook.openLastFailureDetails", "title": "MLSysBook: Open Last Failure Details" }, - { "command": "mlsysbook.setChapterOrderSource", "title": "MLSysBook: Set Chapter Order Source" }, - { "command": "mlsysbook.setQmdVisualPreset", "title": "MLSysBook: Set QMD Visual Preset" }, - { "command": "mlsysbook.openSettings", "title": "MLSysBook: Open Extension Settings" }, + { "command": "mlsysbook.setChapterOrderSource", "title": "Set Chapter Order Source" }, + { "command": "mlsysbook.setQmdVisualPreset", "title": "Set QMD Visual Preset" }, + { "command": "mlsysbook.openSettings", "title": "Open Extension Settings" }, { "command": "mlsysbook.showHealthDetails", "title": "MLSysBook: Show Health Details" } ], "configuration": { @@ -253,6 +253,7 @@ { "command": "mlsysbook.navigatorExpandAll", "when": "view == mlsysbook.navigator", "group": "navigation@2" }, { "command": "mlsysbook.navigatorCollapseAll", "when": "view == mlsysbook.navigator", "group": "navigation@3" }, { "command": "mlsysbook.refreshRunHistory", "when": "view == mlsysbook.runs", "group": "navigation" }, + { "command": "mlsysbook.resetQuartoConfig", "when": "view == mlsysbook.config", "group": "navigation@0" }, { "command": "mlsysbook.setChapterOrderSource", "when": "view == mlsysbook.config", "group": "navigation@1" }, { "command": "mlsysbook.setQmdVisualPreset", "when": "view == mlsysbook.config", "group": "navigation@2" }, { "command": "mlsysbook.openSettings", "when": "view == mlsysbook.config", "group": "navigation@3" } diff --git a/book/vscode-ext/src/commands/buildCommands.ts b/book/vscode-ext/src/commands/buildCommands.ts index 38d2c1f915..1a67d69571 100644 --- a/book/vscode-ext/src/commands/buildCommands.ts +++ b/book/vscode-ext/src/commands/buildCommands.ts @@ -5,6 +5,7 @@ import { discoverChapters } from '../utils/chapters'; import { getRepoRoot, parseQmdFile } from '../utils/workspace'; import { runInVisibleTerminal } from '../utils/terminal'; import { getQuartoResetAllFormatsCommand, withQuartoResetPrefix } from '../utils/quartoConfigReset'; +import { runParallelChapterDebug } from '../utils/parallelDebug'; /** Map volume → PDF filename (derived from book title in Quarto config). */ const PDF_FILENAMES: Record = { @@ -178,6 +179,43 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void { }) ); + // Build All Chapters (Parallel)... (format + workers, then run isolated parallel jobs) + context.subscriptions.push( + vscode.commands.registerCommand('mlsysbook.buildAllChaptersParallel', async (vol: VolumeId) => { + const volumes = discoverChapters(root); + const volume = volumes.find(v => v.id === vol); + if (!volume || volume.chapters.length === 0) { + vscode.window.showWarningMessage(`No chapters found for ${vol}.`); + return; + } + + const fmtPick = await vscode.window.showQuickPick( + [ + { label: 'HTML', id: 'html' as BuildFormat }, + { label: 'PDF', id: 'pdf' as BuildFormat }, + { label: 'EPUB', id: 'epub' as BuildFormat }, + ], + { placeHolder: 'Select format for parallel chapter build' }, + ); + if (!fmtPick) { return; } + + const configDefault = vscode.workspace.getConfiguration('mlsysbook').get('parallelDebugWorkers', 3); + const workerPick = await vscode.window.showQuickPick( + ['1', '2', '3', '4', '6', '8'], + { placeHolder: `Parallel workers (default: ${configDefault})` }, + ); + const workers = workerPick ? Number(workerPick) : configDefault; + + await runParallelChapterDebug({ + repoRoot: root, + volume: vol, + format: fmtPick.id, + chapters: volume.chapters.map(ch => ch.name), + workers, + }); + }), + ); + // Quick Build Current Chapter (PDF) - from currently open .qmd file context.subscriptions.push( vscode.commands.registerCommand('mlsysbook.quickBuildCurrentChapterPdf', () => { diff --git a/book/vscode-ext/src/commands/contextMenuCommands.ts b/book/vscode-ext/src/commands/contextMenuCommands.ts index 3896a75079..3056ef7cc7 100644 --- a/book/vscode-ext/src/commands/contextMenuCommands.ts +++ b/book/vscode-ext/src/commands/contextMenuCommands.ts @@ -2,19 +2,22 @@ import * as vscode from 'vscode'; import { getRepoRoot, parseQmdFile } from '../utils/workspace'; import { runBookCommand } from '../utils/terminal'; import { runIsolatedDebugCommand } from '../utils/parallelDebug'; +import { withQuartoResetPrefix } from '../utils/quartoConfigReset'; +import type { BuildFormat } from '../types'; export function registerContextMenuCommands(context: vscode.ExtensionContext): void { const root = getRepoRoot(); if (!root) { return; } - const makeHandler = (format: string) => { + const makeHandler = (format: BuildFormat) => { return (uri: vscode.Uri) => { const ctx = parseQmdFile(uri); if (!ctx) { vscode.window.showWarningMessage('Could not determine volume/chapter from file path.'); return; } - void runBookCommand(`./book/binder build ${format} ${ctx.chapter} --${ctx.volume} -v`, root, { + const buildCmd = `./book/binder build ${format} ${ctx.chapter} --${ctx.volume} -v`; + void runBookCommand(withQuartoResetPrefix(format, ctx.volume, buildCmd), root, { label: `Build ${format.toUpperCase()} (${ctx.volume}/${ctx.chapter})`, }); }; diff --git a/book/vscode-ext/src/commands/debugCommands.ts b/book/vscode-ext/src/commands/debugCommands.ts index 3069ea88fe..764c050c9f 100644 --- a/book/vscode-ext/src/commands/debugCommands.ts +++ b/book/vscode-ext/src/commands/debugCommands.ts @@ -1,7 +1,6 @@ import * as vscode from 'vscode'; import { VolumeId } from '../types'; -import { getRepoRoot, parseQmdFile } from '../utils/workspace'; -import { runBookCommand } from '../utils/terminal'; +import { getRepoRoot } from '../utils/workspace'; import { discoverChapters } from '../utils/chapters'; import { cancelActiveDebugSession, @@ -9,8 +8,6 @@ import { getLastFailedDebugSession, revealParallelDebugOutput, rerunDebugSession, - runBisectChapterDebug, - runIsolatedDebugCommand, runParallelChapterDebug, } from '../utils/parallelDebug'; @@ -43,7 +40,7 @@ async function runParallelDebugWizard( forcePdf: boolean, ): Promise { const defaultVolume = context.workspaceState.get(STATE_LAST_PARALLEL_VOLUME); - const volumeId = await pickVolume(defaultVolume, 'Select volume for parallel chapter debug'); + const volumeId = await pickVolume(defaultVolume, 'Select volume to test'); if (!volumeId) { return; } const volumes = discoverChapters(root); @@ -60,7 +57,7 @@ async function runParallelDebugWizard( chapter: ch.name, })), { - placeHolder: 'Select chapters to debug in parallel', + placeHolder: 'Select chapters to test (multi-select)', canPickMany: true, matchOnDescription: true, }, @@ -99,121 +96,17 @@ export function registerDebugCommands(context: vscode.ExtensionContext): void { const root = getRepoRoot(); if (!root) { return; } - // Debug full volume + // Test All Chapters (Parallel) - main debug entry point context.subscriptions.push( - vscode.commands.registerCommand('mlsysbook.debugVolumePdf', (vol: VolumeId) => { - void runIsolatedDebugCommand({ - repoRoot: root, - command: `./book/binder debug pdf --${vol}`, - label: `Debug Volume PDF (${vol})`, - }); - }) - ); - - // Debug chapter sections (interactive QuickPick) - context.subscriptions.push( - vscode.commands.registerCommand('mlsysbook.debugChapterSections', async () => { - const volPick = await vscode.window.showQuickPick( - [ - { label: 'Volume I', id: 'vol1' as VolumeId }, - { label: 'Volume II', id: 'vol2' as VolumeId }, - ], - { placeHolder: 'Select volume' }, - ); - if (!volPick) { return; } - - const volumes = discoverChapters(root); - const volume = volumes.find(v => v.id === volPick.id); - if (!volume) { return; } - - const chapterPick = await vscode.window.showQuickPick( - volume.chapters.map(ch => ({ label: ch.displayName, description: ch.name, id: ch.name })), - { placeHolder: 'Select chapter to debug' }, - ); - if (!chapterPick) { return; } - - const fmtPick = await vscode.window.showQuickPick( - ['pdf', 'html', 'epub'], - { placeHolder: 'Select format (default: pdf)' }, - ); - const fmt = fmtPick ?? 'pdf'; - - void runIsolatedDebugCommand({ - repoRoot: root, - command: `./book/binder debug ${fmt} --${volPick.id} --chapter ${chapterPick.id}`, - label: `Debug Chapter ${fmt.toUpperCase()} (${volPick.id}/${chapterPick.id})`, - }); - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand('mlsysbook.debugParallelChapters', async () => { + vscode.commands.registerCommand('mlsysbook.testAllChaptersParallel', async () => { await runParallelDebugWizard(root, context, false); }) ); + // Keep debugParallelChapters as alias for backward compatibility (e.g. command palette) context.subscriptions.push( - vscode.commands.registerCommand('mlsysbook.debugBisectChapters', async () => { - const defaultVolume = context.workspaceState.get(STATE_LAST_PARALLEL_VOLUME); - const volumeId = await pickVolume(defaultVolume, 'Select volume for bisect debug'); - if (!volumeId) { return; } - - const volumes = discoverChapters(root); - const volume = volumes.find(v => v.id === volumeId); - if (!volume || volume.chapters.length === 0) { - vscode.window.showWarningMessage(`No chapters found for ${volumeId}.`); - return; - } - - const candidateMode = await vscode.window.showQuickPick( - [ - { label: 'Use all chapters in this volume', id: 'all' }, - { label: 'Select candidate chapters manually', id: 'manual' }, - ], - { placeHolder: 'Choose bisect candidate set' }, - ); - if (!candidateMode) { return; } - - let chapters = volume.chapters.map(ch => ch.name); - if (candidateMode.id === 'manual') { - const picks = await vscode.window.showQuickPick( - volume.chapters.map(ch => ({ - label: ch.displayName, - description: ch.name, - chapter: ch.name, - })), - { - placeHolder: 'Select candidate chapters for bisect', - canPickMany: true, - matchOnDescription: true, - }, - ); - if (!picks || picks.length === 0) { return; } - chapters = picks.map(p => p.chapter); - } - - const fmtPick = await vscode.window.showQuickPick( - ['pdf', 'html', 'epub'], - { placeHolder: 'Select debug format (default: pdf)' }, - ); - const fmt = fmtPick ?? 'pdf'; - - const defaultWorkers = vscode.workspace - .getConfiguration('mlsysbook') - .get('parallelDebugWorkers', 3); - const workers = await pickWorkers(defaultWorkers); - if (!workers) { return; } - - await context.workspaceState.update(STATE_LAST_PARALLEL_VOLUME, volumeId); - await context.workspaceState.update(STATE_LAST_PARALLEL_WORKERS, workers); - - await runBisectChapterDebug({ - repoRoot: root, - volume: volumeId, - format: fmt, - chapters, - workers, - }); + vscode.commands.registerCommand('mlsysbook.debugParallelChapters', async () => { + await runParallelDebugWizard(root, context, false); }) ); @@ -277,24 +170,4 @@ export function registerDebugCommands(context: vscode.ExtensionContext): void { }), ); - context.subscriptions.push( - vscode.commands.registerCommand('mlsysbook.quickBuildCurrentChapterPdf', () => { - const uri = vscode.window.activeTextEditor?.document.uri; - if (!uri || !uri.fsPath.endsWith('.qmd')) { - vscode.window.showWarningMessage('Open a chapter .qmd file to run quick chapter PDF build.'); - return; - } - const parsed = parseQmdFile(uri); - if (!parsed) { - vscode.window.showWarningMessage('Could not determine volume/chapter for active file.'); - return; - } - void runBookCommand(`./book/binder build pdf ${parsed.chapter} --${parsed.volume} -v`, root, { - label: `Quick Chapter PDF (${parsed.volume}/${parsed.chapter})`, - }); - }), - vscode.commands.registerCommand('mlsysbook.quickParallelDebugSelectedPdf', async () => { - await runParallelDebugWizard(root, context, true); - }), - ); } diff --git a/book/vscode-ext/src/commands/precommitCommands.ts b/book/vscode-ext/src/commands/precommitCommands.ts index c963d7c9a0..a03fa0313d 100644 --- a/book/vscode-ext/src/commands/precommitCommands.ts +++ b/book/vscode-ext/src/commands/precommitCommands.ts @@ -3,12 +3,22 @@ import * as path from 'path'; import { PRECOMMIT_QMD_FILE_FIXERS } from '../constants'; import { getRepoRoot } from '../utils/workspace'; import { runBookCommand } from '../utils/terminal'; +import type { PrecommitStatusManager } from '../validation/precommitStatusManager'; function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } -export function registerPrecommitCommands(context: vscode.ExtensionContext): void { +/** Extract hook id from pre-commit command, e.g. "pre-commit run codespell --all-files" -> "codespell" */ +function parseHookIdFromCommand(command: string): string | null { + const match = command.match(/pre-commit run ([a-zA-Z0-9_-]+)/); + return match?.[1] ?? null; +} + +export function registerPrecommitCommands( + context: vscode.ExtensionContext, + statusManager: PrecommitStatusManager, +): void { const root = getRepoRoot(); if (!root) { return; } @@ -44,14 +54,25 @@ export function registerPrecommitCommands(context: vscode.ExtensionContext): voi context.subscriptions.push( vscode.commands.registerCommand('mlsysbook.precommitRunAll', () => { + statusManager.setRunAllPending(); void vscode.workspace.saveAll(false).then(() => runBookCommand('pre-commit run --all-files', root, { label: 'Pre-commit (all hooks)', + onComplete: (success) => statusManager.setRunAllResult(success), })); }), vscode.commands.registerCommand('mlsysbook.precommitRunHook', (command: string) => { + const hookId = parseHookIdFromCommand(command); + if (hookId) { + statusManager.setHookPending(hookId); + } void vscode.workspace.saveAll(false).then(() => runBookCommand(command, root, { label: 'Pre-commit (selected hook)', + onComplete: (success) => { + if (hookId) { + statusManager.setHookResult(hookId, success); + } + }, })); }), diff --git a/book/vscode-ext/src/extension.ts b/book/vscode-ext/src/extension.ts index 8ce963950e..918cc23b0a 100644 --- a/book/vscode-ext/src/extension.ts +++ b/book/vscode-ext/src/extension.ts @@ -30,6 +30,7 @@ import { } from './utils/terminal'; import { ChapterOrderSource } from './types'; import { HealthManager, HealthStatus } from './validation/healthManager'; +import { PrecommitStatusManager } from './validation/precommitStatusManager'; export function activate(context: vscode.ExtensionContext): void { const root = getRepoRoot(); @@ -57,6 +58,10 @@ export function activate(context: vscode.ExtensionContext): void { const healthManager = new HealthManager(); precommitProvider.setHealthManager(healthManager); + // Pre-commit status (for check/cross icons in precommit tree) + const precommitStatusManager = new PrecommitStatusManager(); + precommitProvider.setPrecommitStatusManager(precommitStatusManager); + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); statusBarItem.command = 'mlsysbook.showHealthDetails'; updateStatusBar(statusBarItem, healthManager); @@ -67,7 +72,7 @@ export function activate(context: vscode.ExtensionContext): void { precommitProvider.refresh(); }); - context.subscriptions.push(healthManager, statusBarItem); + context.subscriptions.push(healthManager, precommitStatusManager, statusBarItem); // Inline Python value resolution (hover, ghost text, CodeLens) let pythonResolver: QmdPythonValueResolver | undefined; @@ -249,7 +254,7 @@ export function activate(context: vscode.ExtensionContext): void { // Register all command groups registerBuildCommands(context); registerDebugCommands(context); - registerPrecommitCommands(context); + registerPrecommitCommands(context, precommitStatusManager); registerPublishCommands(context); registerContextMenuCommands(context); diff --git a/book/vscode-ext/src/providers/buildTreeProvider.ts b/book/vscode-ext/src/providers/buildTreeProvider.ts index c6ff5703bf..b6c138fd9a 100644 --- a/book/vscode-ext/src/providers/buildTreeProvider.ts +++ b/book/vscode-ext/src/providers/buildTreeProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { VolumeInfo, BuildFormat } from '../types'; +import { VolumeInfo } from '../types'; import { discoverChapters } from '../utils/chapters'; import { VolumeTreeItem, ChapterTreeItem, ActionTreeItem } from '../models/treeItems'; @@ -37,17 +37,13 @@ export class BuildTreeProvider implements vscode.TreeDataProvider { const chapterItems = vol.chapters.map(ch => new ChapterTreeItem(ch)); - const formats: BuildFormat[] = ['html', 'pdf', 'epub']; - const volumeActions = formats.map(fmt => - new ActionTreeItem( - `Build Full ${vol.label} (${fmt.toUpperCase()})`, - `mlsysbook.buildVolume${fmt.charAt(0).toUpperCase() + fmt.slice(1)}`, - [vol.id], - 'package', - ) - ); + const volumeActions = [ + new ActionTreeItem('Build Full Volume...', 'mlsysbook.buildFullVolume', [vol.id], 'package'), + new ActionTreeItem('Build Chapters...', 'mlsysbook.buildSelectedChapters', [vol.id], 'files'), + new ActionTreeItem('Build All Chapters (Parallel)...', 'mlsysbook.buildAllChaptersParallel', [vol.id], 'beaker'), + ]; - return [...chapterItems, ...volumeActions]; + return [...volumeActions, ...chapterItems]; } if (element instanceof ChapterTreeItem) { diff --git a/book/vscode-ext/src/providers/configTreeProvider.ts b/book/vscode-ext/src/providers/configTreeProvider.ts index 47b9cdc1a0..5b8bb52b6a 100644 --- a/book/vscode-ext/src/providers/configTreeProvider.ts +++ b/book/vscode-ext/src/providers/configTreeProvider.ts @@ -11,6 +11,7 @@ export class ConfigTreeProvider implements vscode.TreeDataProvider private _healthManager: HealthManager | undefined; private _healthSummary: HealthSummaryItem | undefined; + private _precommitStatusManager: PrecommitStatusManager | undefined; /** Wire up the health manager (called from extension.ts). */ setHealthManager(manager: HealthManager): void { this._healthManager = manager; } + /** Wire up pre-commit status (called from extension.ts). */ + setPrecommitStatusManager(manager: PrecommitStatusManager): void { + this._precommitStatusManager = manager; + manager.onDidChange(() => this.refresh()); + } + refresh(): void { this._healthSummary = undefined; // force rebuild this._onDidChangeTreeData.fire(undefined); @@ -143,20 +177,23 @@ export class PrecommitTreeProvider implements vscode.TreeDataProvider items.push(new SeparatorItem('')); } - const runAll = new ActionTreeItem( + const runAllStatus = this._precommitStatusManager?.runAllStatus ?? 'pending'; + const runAll = new PrecommitActionItem( 'Run ALL Hooks', 'mlsysbook.precommitRunAll', [], - 'checklist', + runAllStatus, ); - const checkItems = PRECOMMIT_CHECK_HOOKS.map(h => - new ActionTreeItem(h.label, 'mlsysbook.precommitRunHook', [h.command], 'play') - ); + const checkItems = PRECOMMIT_CHECK_HOOKS.map(h => { + const status = this._precommitStatusManager?.getHookStatus(h.id) ?? 'pending'; + return new PrecommitActionItem(h.label, 'mlsysbook.precommitRunHook', [h.command], status); + }); - const fixerItems = PRECOMMIT_FIXER_HOOKS.map(h => - new ActionTreeItem(h.label, 'mlsysbook.precommitRunHook', [h.command], 'wrench') - ); + const fixerItems = PRECOMMIT_FIXER_HOOKS.map(h => { + const status = this._precommitStatusManager?.getHookStatus(h.id) ?? 'pending'; + return new PrecommitActionItem(h.label, 'mlsysbook.precommitRunHook', [h.command], status); + }); const currentFileFixers = new ActionTreeItem( 'Run QMD Fixers (Current File)', diff --git a/book/vscode-ext/src/utils/chapters.ts b/book/vscode-ext/src/utils/chapters.ts index d01b600122..56e22c217d 100644 --- a/book/vscode-ext/src/utils/chapters.ts +++ b/book/vscode-ext/src/utils/chapters.ts @@ -70,13 +70,18 @@ function readChapterOrderFromConfig(repoRoot: string, vol: VolumeId, source: Cha continue; } - // Regular chapter directories: contents/volX//.qmd + // Regular chapter entries: use the chapter file stem as command key. const dirName = segments[0]; - if (EXCLUDED_CHAPTER_DIRS.has(dirName) || seen.has(dirName)) { + if (EXCLUDED_CHAPTER_DIRS.has(dirName)) { continue; } - seen.add(dirName); - chapterOrderKeys.push(dirName); + const fileNameOnly = segments[segments.length - 1]; + const chapterStem = fileNameOnly.replace(/\.qmd$/i, ''); + if (seen.has(chapterStem)) { + continue; + } + seen.add(chapterStem); + chapterOrderKeys.push(chapterStem); } if (chapterOrderKeys.length > 0) { @@ -108,12 +113,28 @@ export function discoverChapters(repoRoot: string): VolumeInfo[] { const dirPath = path.join(contentsDir, e.name); return fs.readdirSync(dirPath).some(f => f.endsWith('.qmd')); }) - .map(e => ({ - name: e.name, - volume: vol, - dirPath: path.join(contentsDir, e.name), - displayName: toDisplayName(e.name), - })); + .map(e => { + const dirPath = path.join(contentsDir, e.name); + const qmdFiles = fs.readdirSync(dirPath) + .filter(f => f.endsWith('.qmd')) + .sort(); + + const preferredByConfig = qmdFiles.find(file => { + const stem = file.replace(/\.qmd$/i, ''); + return orderByName.has(stem); + }); + const preferredByDirName = qmdFiles.find(file => file.replace(/\.qmd$/i, '') === e.name); + const preferredNonHidden = qmdFiles.find(file => !path.basename(file).startsWith('_')); + const selected = preferredByConfig ?? preferredByDirName ?? preferredNonHidden ?? qmdFiles[0]; + const chapterStem = selected.replace(/\.qmd$/i, ''); + + return { + name: chapterStem, + volume: vol, + dirPath, + displayName: toDisplayName(chapterStem), + }; + }); const appendixDir = path.join(contentsDir, 'backmatter'); const appendixChapters: ChapterInfo[] = fs.existsSync(appendixDir) diff --git a/book/vscode-ext/src/utils/parallelDebug.ts b/book/vscode-ext/src/utils/parallelDebug.ts index 95578e6319..40bfd869b1 100644 --- a/book/vscode-ext/src/utils/parallelDebug.ts +++ b/book/vscode-ext/src/utils/parallelDebug.ts @@ -341,11 +341,11 @@ async function runParallelDebugBatch(options: BatchRunOptions): Promise 0) { channel.appendLine(`Failed chapters: ${failed.join(', ')}`); + const worktrees = Object.entries(session.failedWorktrees); + if (worktrees.length > 0) { + channel.appendLine('Failed worktrees (kept for inspection):'); + for (const [chapter, worktreePath] of worktrees) { + channel.appendLine(` ${chapter}: ${worktreePath}`); + } + } } if (session.failureLocations.length > 0) { channel.appendLine('Top failure locations:'); diff --git a/book/vscode-ext/src/utils/quartoConfigReset.ts b/book/vscode-ext/src/utils/quartoConfigReset.ts new file mode 100644 index 0000000000..07daf99d5b --- /dev/null +++ b/book/vscode-ext/src/utils/quartoConfigReset.ts @@ -0,0 +1,25 @@ +/** + * Helpers for resetting Quarto config YAML (uncomment all chapter/appendix entries). + * Delegates to the binder: ./book/binder reset [--vol1|--vol2] + */ + +import type { BuildFormat, VolumeId } from '../types'; + +/** Reset command for one format and one volume (e.g. "pdf reset --vol1"). */ +export function getQuartoResetCommand(format: BuildFormat, volume: VolumeId): string { + return `./book/binder ${format} reset --${volume}`; +} + +/** Reset command for one format, both volumes (used by "Reset Quarto config"). */ +export function getQuartoResetAllFormatsCommand(): string { + return './book/binder pdf reset && ./book/binder html reset && ./book/binder epub reset'; +} + +/** Prefix to run before a build so config is uncommented: reset then build. */ +export function withQuartoResetPrefix( + format: BuildFormat, + volume: VolumeId, + buildCommand: string, +): string { + return `${getQuartoResetCommand(format, volume)} && ${buildCommand}`; +} diff --git a/book/vscode-ext/src/utils/terminal.ts b/book/vscode-ext/src/utils/terminal.ts index 483ba5cc39..2c8e142ef6 100644 --- a/book/vscode-ext/src/utils/terminal.ts +++ b/book/vscode-ext/src/utils/terminal.ts @@ -35,9 +35,11 @@ export interface CommandRunRecord { mode: 'raw' | 'focused'; } -interface RunOptions { +export interface RunOptions { mode?: ExecutionMode; label?: string; + /** When provided, runs in focused mode to capture exit code, then calls with success. */ + onComplete?: (success: boolean) => void; } let extensionContext: vscode.ExtensionContext | undefined; @@ -164,7 +166,7 @@ export function initializeRunManager(context: vscode.ExtensionContext): void { } export async function runBookCommand(command: string, cwd: string, options: RunOptions = {}): Promise { - const mode = options.mode ?? getExecutionMode(); + const mode = options.onComplete ? 'focused' : (options.mode ?? getExecutionMode()); const label = options.label ?? 'MLSysBook command'; const runId = Date.now(); const start = Date.now(); @@ -222,6 +224,8 @@ export async function runBookCommand(command: string, cwd: string, options: RunO channel.appendLine(makeSummary(runId, label, success, elapsedMs, command)); channel.appendLine(''); + options.onComplete?.(success); + if (success) { void vscode.window.showInformationMessage(`${label} succeeded.`); return; diff --git a/book/vscode-ext/src/validation/precommitStatusManager.ts b/book/vscode-ext/src/validation/precommitStatusManager.ts new file mode 100644 index 0000000000..af6a7d7dc0 --- /dev/null +++ b/book/vscode-ext/src/validation/precommitStatusManager.ts @@ -0,0 +1,47 @@ +/** + * Tracks last run status for pre-commit hooks so the tree can show check/cross icons. + */ + +import * as vscode from 'vscode'; + +export type HookStatus = 'pending' | 'pass' | 'fail'; + +export class PrecommitStatusManager implements vscode.Disposable { + private readonly _onDidChange = new vscode.EventEmitter(); + readonly onDidChange = this._onDidChange.event; + + private _runAllStatus: HookStatus = 'pending'; + private _hookStatuses = new Map(); + + get runAllStatus(): HookStatus { + return this._runAllStatus; + } + + getHookStatus(hookId: string): HookStatus { + return this._hookStatuses.get(hookId) ?? 'pending'; + } + + setRunAllResult(success: boolean): void { + this._runAllStatus = success ? 'pass' : 'fail'; + this._onDidChange.fire(); + } + + setHookResult(hookId: string, success: boolean): void { + this._hookStatuses.set(hookId, success ? 'pass' : 'fail'); + this._onDidChange.fire(); + } + + setRunAllPending(): void { + this._runAllStatus = 'pending'; + this._onDidChange.fire(); + } + + setHookPending(hookId: string): void { + this._hookStatuses.set(hookId, 'pending'); + this._onDidChange.fire(); + } + + dispose(): void { + this._onDidChange.dispose(); + } +} diff --git a/current_plot.png b/current_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e29ec60ad640250b3356f1065e6c460309be5e GIT binary patch literal 47272 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 literal 0 HcmV?d00001 diff --git a/proposed_plot.png b/proposed_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a1facd81dda66d389414d1baeb3b73e87dec1d GIT binary patch literal 48542 zcmeFYbyQSe{PznAC?y7sgi_MdT_Ro5T~gBBDM(04jxIgs%Lxey$LQ`4a)EHZ1rkydhV)xeRrl1L*$0{` z=F<=Mg}u6Bci-SXp79!hN>;bkxau?r@$Zn;X!BcF5AU5c`!pcF*SNr&%=KO^{0#EP zc-7VKHaqa*#mCH0DTS_EH@yMVr;ob3r9@1r8w5?wmp0D_Z4I4 zukiNoE9XAe;OEf4FK2&bzW?5S^jd`W-?xU4(Kh~l@-5#_q_lsZlzKpz`QMQ?k?uSH zcLLJ?bI|`kjj3u|^UZ}74=?ZHV3rtE=rkrKrf$2e!McgW^yE_Ygk6hs3 z?&)a7CKzK*n=37cz1ntEjln+91cv%e&>yY|z};gZSaPtL;KgL`38* z7#g|}X5Yl-Cj0g+O5d@$O= z_H1)`IWQ-O3X5Fmd2@4f-EPga+{cSJMWN3Uk;H`yv|L>Hb}iRjSZIOmJ~9|}IsLc$ zZ8(-B()sX-AC&54x>|b&wKmgv3dCpU=Y^f5uMLsL$_?y($cu`8Os0O^fGw zvK1sAj<01+Ku#V_Vq1s&@#9BJe%>bshS_?Uq@_^}191ljhlJ>? z-kTcBQHj^z^wp==?BMRX8;Ou79~JhH{HD zmCvP!kE|kNT-OO(Awx)S@5fqSO2}N3SMf8h!#P6M_n-LE*COCq#+^}%Q�_5V1FJ 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 literal 0 HcmV?d00001