From 05d2263a843e4ec7b60f39b35e08f468e7fa1972 Mon Sep 17 00:00:00 2001 From: Jason Andrews Date: Fri, 1 May 2026 13:46:55 -0500 Subject: [PATCH] Use the Arm MCP server to optimize with SVE Co-authored-by: Copilot --- .../adler32-kiro/1-overview.md | 73 ++++ .../adler32-kiro/2-baseline.md | 292 ++++++++++++++ .../adler32-kiro/3-sve-concepts.md | 358 ++++++++++++++++++ .../adler32-kiro/4-nmax-optimization.md | 191 ++++++++++ .../adler32-kiro/5-sve-implementation.md | 301 +++++++++++++++ .../adler32-kiro/6-results.md | 87 +++++ .../adler32-kiro/_index.md | 75 ++++ .../adler32-kiro/_next-steps.md | 8 + 8 files changed, 1385 insertions(+) create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/1-overview.md create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/2-baseline.md create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/3-sve-concepts.md create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/4-nmax-optimization.md create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/5-sve-implementation.md create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/6-results.md create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/_index.md create mode 100644 content/learning-paths/servers-and-cloud-computing/adler32-kiro/_next-steps.md diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/1-overview.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/1-overview.md new file mode 100644 index 0000000000..ab0f43dd92 --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/1-overview.md @@ -0,0 +1,73 @@ +--- +title: Overview of the Adler-32 algorithm and optimization approach +weight: 2 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## The optimization task + +You'll take a simple scalar implementation of the Adler-32 checksum algorithm written in C and incrementally optimize it to use Arm Scalable Vector Extension (SVE) intrinsics. The final SVE version runs significantly faster than the original scalar code on Neoverse processors. + +What makes this Learning Path different from a typical optimization tutorial is how you'll get there. Rather than being handed a finished SVE implementation, you'll use an AI coding assistant connected to the Arm MCP server to guide you through each step. You'll ask questions, look up intrinsics, understand the algorithm's constraints, and build the solution piece by piece. + +AI coding assistants are not yet able to automatically generate optimized code, but you can use them to guide your learning and the implementation details. This way, you can maintain and explain the code and arrive at optimized solutions. This process mirrors what you'd do on your own projects. + +## The Adler-32 algorithm + +Adler-32 is a checksum algorithm used to verify data integrity. It is used in the zlib compression format. It's fast, simple, and a good candidate for vectorization because its inner loop processes one byte at a time. + +The algorithm maintains two 16-bit accumulators, `A` and `B`: + +- `A` starts at 1 and accumulates the sum of all input bytes +- `B` accumulates the running sum of all `A` values + +Both are taken modulo 65521, the largest prime smaller than 2^16. The final checksum is `(B << 16) | A`. + +The scalar implementation is straightforward: + +```c +#define MOD_ADLER 65521 + +uint32_t adler32(const uint8_t *data, size_t len) +{ + uint32_t a = 1; + uint32_t b = 0; + + for (size_t i = 0; i < len; i++) { + a = (a + data[i]) % MOD_ADLER; + b = (b + a) % MOD_ADLER; + } + + return (b << 16) | a; +} +``` + +This loop has two characteristics that make it interesting to vectorize: + +- The `a` accumulator is a simple sum that parallelizes well +- The `b` accumulator depends on the running value of `a` after each byte, which makes it harder to vectorize + +You'll learn how SVE intrinsics solve both of these challenges. + +## The role of the Arm MCP server + +The Arm MCP server gives your AI coding assistant access to Arm-specific knowledge, including the full SVE intrinsics reference. When you ask about specific intrinsics like `svdot` or `svwhilelt`, the assistant queries the MCP server and returns the exact function signature, pseudocode, and required compiler flags. + +This means you don't need to keep opening the intrinsics reference material. You can ask questions in plain language and get precise, actionable answers grounded in the actual Arm documentation. + +## Outline of each section + +Each section follows a consistent pattern: + +1. A short explanation of what you need to understand at this stage +2. Suggested prompts to ask your AI assistant +3. An explanation of what to look for in the response +4. The code or configuration changes that result from the conversation + +You can follow along exactly, or adapt the prompts to your own style. The goal is to learn the process of using an AI assistant to apply SVE and achieve improved performance. + +## What's next + +Start by setting up the project and establishing a performance baseline for the scalar implementation. The baseline is required before you can measure any improvement. diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/2-baseline.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/2-baseline.md new file mode 100644 index 0000000000..c41a9ed1ce --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/2-baseline.md @@ -0,0 +1,292 @@ +--- +title: Set up the project and establish a baseline +weight: 3 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Before you begin + +To get started, you need an Arm Linux system with SVE support. Suitable cloud instances include AWS Graviton3 or Graviton4, Microsoft Cobalt 100, or Google Axion. The examples in this Learning Path were tested on Ubuntu 26.04. + +You also need an AI coding assistant with the Arm MCP server configured. Supported assistants include [GitHub Copilot](/install-guides/github-copilot/), [Kiro CLI](/install-guides/kiro-cli/), [Claude Code](/install-guides/claude-code/), [Gemini CLI](/install-guides/gemini/), and [Codex CLI](/install-guides/codex-cli/). See the [Arm MCP server Learning Path](/learning-paths/servers-and-cloud-computing/arm-mcp-server/) for setup instructions. + +{{< notice Note >}} +The AI responses shown are samples. Your AI assistant may word responses differently, include more or less detail, or structure the output differently depending on the tool and model you are using. Focus on the key concepts rather than the exact wording. +{{< /notice >}} + +Start by installing the required software and check your system includes SVE. + +Install GCC and GNU Make: + +```bash +sudo apt install gcc make -y +``` + +Confirm your system has SVE by checking the CPU flags: + +```bash +lscpu | grep -i sve +``` + +The output should include `sve` in the flags list: + +```output +Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti +``` + +If `sve` does not appear, the system does not support SVE and the final implementation in this Learning Path won't run correctly. + +## Create the project files + +On your Arm Neoverse system, create a working directory and add the source files. + +```bash +mkdir adler32-sve && cd adler32-sve +``` + +Use an editor to copy the scalar implementation to `adler32-simple.c`: + +```c +/* + * Adler-32 checksum — simple scalar C implementation + */ +#include +#include + +#define MOD_ADLER 65521 + +uint32_t adler32(const uint8_t *data, size_t len) +{ + uint32_t a = 1; + uint32_t b = 0; + + for (size_t i = 0; i < len; i++) { + a = (a + data[i]) % MOD_ADLER; + b = (b + a) % MOD_ADLER; + } + + return (b << 16) | a; +} +``` + +Create the test and benchmark harness in `adler32-test.c`: + +```c +/* + * Adler-32 test and benchmark harness + */ +#include +#include +#include +#include +#include + +extern uint32_t adler32(const uint8_t *data, size_t len); + +static void fill_random(uint8_t *buf, size_t len, uint32_t seed) +{ + for (size_t i = 0; i < len; i++) { + seed = seed * 1103515245 + 12345; + buf[i] = (uint8_t)(seed >> 16); + } +} + +static double time_sec(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec * 1e-9; +} + +static int test_known_vectors(void) +{ + /* RFC 1950: adler32("Wikipedia") = 0x11E60398 */ + const uint8_t wiki[] = "Wikipedia"; + uint32_t got = adler32(wiki, 9); + uint32_t expect = 0x11E60398; + + printf("Correctness: adler32(\"Wikipedia\") = 0x%08X ", got); + if (got == expect) { + printf("[PASS]\n"); + return 0; + } else { + printf("[FAIL] expected 0x%08X\n", expect); + return 1; + } +} + +static void benchmark(const char *label, size_t size, int iters) +{ + uint8_t *data = malloc(size); + if (!data) { perror("malloc"); exit(1); } + fill_random(data, size, (uint32_t)size); + + volatile uint32_t sink = adler32(data, size); + (void)sink; + + double t0 = time_sec(); + for (int i = 0; i < iters; i++) + sink = adler32(data, size); + double elapsed = time_sec() - t0; + + double mb = (double)size * iters / (1024.0 * 1024.0); + printf(" %-8s %8zu bytes %6d iters %8.3f ms %8.1f MB/s checksum=0x%08X\n", + label, size, iters, elapsed * 1000.0, mb / elapsed, sink); + + free(data); +} + +int main(void) +{ + int fail = test_known_vectors(); + if (fail) return 1; + + printf("\nPerformance:\n"); + benchmark("1 KB", 1024, 100000); + benchmark("10 KB", 10 * 1024, 10000); + benchmark("100 KB", 100 * 1024, 1000); + benchmark("1 MB", 1024 * 1024, 100); + benchmark("10 MB", 10 * 1024 * 1024, 10); + + return 0; +} +``` + +Create a `Makefile`: + +```makefile +CC = gcc +CFLAGS = -O3 -mcpu=native -flto -Wall -Wextra +LDFLAGS = -flto +TARGET = adler32-test +SRCS = adler32-simple.c adler32-test.c + +$(TARGET): $(SRCS) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(SRCS) + +run: $(TARGET) + ./$(TARGET) + +clean: + rm -f $(TARGET) + +.PHONY: run clean +``` + +The `-mcpu=native` flag tells GCC to optimize for the exact CPU you're running on, which enables SVE code generation on Neoverse processors that have SVE. + +### ASK AI: about compiler flags + +Before running anything, ask your AI assistant to confirm that your build setup is correct for SVE. + +```text +My Makefile uses `-O3 -mcpu=native`. Does this enable SVE code generation on a Neoverse processor? Do I need any special flags for SVE intrinsics? +``` + +A sample response is: + +```output +Good question. Let me check what CPU is on your remote system and look up the relevant details. + +Here's the full picture: + +Yes, `-O3 -mcpu=native` enables SVE code generation on your system. Your Neoverse-V2 processor supports SVE and SVE2, and GCC 15.2 +with -mcpu=native resolves to armv9-a+sve2-aes+sve2-bitperm+sve2-sha3+.... I confirmed the compiler is already auto-vectorizing +with SVE instructions (4 SVE instructions emitted for a simple dot-product loop). + +For SVE intrinsics, you need: + +1. No extra compiler flags — -mcpu=native already enables SVE/SVE2. You don't need to add -march=armv8-a+sve separately (that + would actually be a downgrade from what -mcpu=native gives you on this CPU). +2. Include the header in your source files: + + #include + +That's it. Your current Makefile flags are correct for both auto-vectorized SVE code and hand-written SVE intrinsics. + +One caveat: since -mcpu=native detects the build machine's CPU, binaries built this way may not run on older Arm processors (e.g., +Neoverse-N1/Graviton2). If portability matters, use -mcpu=neoverse-v2 explicitly to make the target clear. + +For more on SVE programming, Arm has a good learning path: Port Code to Arm SVE +(https://learn.arm.com/learning-paths/servers-and-cloud-computing/sve/). +``` + +The response explains that `-mcpu=native` enables SVE. It also provides useful info about running on other systems and confirm special flags, such as `-march=armv8-a+sve` are not needed. The response also tells you to include ``. + +You also notice the reference to a Learning Path about SVE at the end. This confirms the Arm MCP server is consulted on answering the question. + +This is good information you'll need when you create the SVE source later. + +## Build and run the baseline + +Build and run the scalar version: + +```bash +make run +``` + +The output is similar to: + +```output +Correctness: adler32("Wikipedia") = 0x11E60398 [PASS] + +Performance: + 1 KB 1024 bytes 100000 iters 255.619 ms 382.0 MB/s checksum=0x37B4063C + 10 KB 10240 bytes 10000 iters 252.763 ms 386.4 MB/s checksum=0x5FA40D73 + 100 KB 102400 bytes 1000 iters 255.239 ms 382.6 MB/s checksum=0x2378D74C + 1 MB 1048576 bytes 100 iters 261.659 ms 382.2 MB/s checksum=0x058B4B08 + 10 MB 10485760 bytes 10 iters 262.388 ms 381.1 MB/s checksum=0x285FF1B1 +``` + +Your numbers will differ depending on your specific Neoverse processor and memory configuration. Make a note of the MB/s values for the 1 MB and 10 MB cases, as these are your baseline numbers to compare against after each optimization. + +### ASK AI: about auto-vectorization + +Now ask your AI assistant a question that many developers wonder about: + +```text +Can GCC auto-vectorize my adler32 function with SVE if I just use `-mcpu=native`? What would prevent auto-vectorization? +``` + +A sample response is: + +```output +No, GCC cannot auto-vectorize your adler32 function. It tried every vector mode (including SVE's VNx* modes) and failed on all of + them. + + The report tells us exactly why: "not vectorized: unsupported use in stmt" on line 16. + + The problem is the modulo operation inside the loop: + + a = (a + data[i]) % MOD_ADLER; // ← loop-carried dependency + modulo + b = (b + a) % MOD_ADLER; // ← depends on 'a' above, also modulo + + Two things kill auto-vectorization here: + + 1. Loop-carried dependency chain: Each iteration's b depends on the current iteration's a, and each a depends on the previous a. + The compiler can't compute multiple iterations in parallel because every result feeds into the next. + 2. Modulo by a non-power-of-2: % 65521 is an integer division/remainder — there's no cheap SIMD equivalent. The compiler can't + vectorize this operation across lanes. + + This is why hand-written SVE intrinsics are valuable here. The standard trick is to: + + - Defer the modulo — accumulate partial sums across SVE lanes without reducing mod 65521 every iteration, only applying the modulo + periodically (every ~5552 bytes, the largest safe batch before uint32_t overflow). + - Break the dependency — use vector lanes to accumulate a and b contributions independently, then reduce at the end. +``` + +The response explains that the modulo operation in every iteration (`% MOD_ADLER`) is the main blocker. The compiler can't easily prove that the intermediate values won't overflow in a way that changes the result when operations are reordered. The loop-carried dependency between iterations also makes it difficult. + +Since auto-vectorization won't work, you need to restructure the algorithm before SVE can be applied effectively. The restructuring is explained in the next two sections. + +## What you've learned and what's next + +In this section: + +- You created the scalar Adler-32 implementation and benchmark harness +- You recorded your baseline performance numbers +- You learned that auto-vectorization won't work + +In the next section, you'll use the Arm MCP server to learn the core SVE concepts you need before writing any intrinsics code. diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/3-sve-concepts.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/3-sve-concepts.md new file mode 100644 index 0000000000..ceb7705876 --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/3-sve-concepts.md @@ -0,0 +1,358 @@ +--- +title: Understand core SVE concepts for vectorization +weight: 4 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## SVE concepts you need before writing code + +SVE is different from fixed width SIMD like Neon. The vector length is not fixed at compile time. It is determined at runtime by the hardware. This means you can't write `for (i = 0; i < n; i += 16)` and assume you're processing 16 bytes per iteration. SVE code must be vector length agnostic (VLA) to work correctly on any processor with SVE support. + +Before writing SVE intrinsics, it's helpful to understand three things: + +1. How SVE predicates control which elements are active +2. How to handle loop tails when data length isn't a multiple of the vector length +3. How to widen narrow data types for accumulation + +The Arm MCP server is the right tool for this. Ask your AI assistant the questions below and read the responses. The goal isn't to memorize intrinsic names, but to understand the concepts well enough to recognize when you need each one. + +## Comparing SVE and Neon + +Start with understanding the big picture about SVE. + +### ASK AI: about SVE versus Neon + +Ask your assistant: + +```text +Ask the Arm MCP server what is SVE and how does it differ from Neon? My Makefile targets the native CPU on a Neoverse processor. +``` + +A sample response is: + +```output +Here's what the Arm knowledge base says: + + SVE vs Neon + + Neon (also called ASIMD/Advanced SIMD) is Arm's original SIMD extension. It uses fixed 128-bit vector registers (Q0–Q31). Every + Neon instruction operates on exactly 128 bits — if your data doesn't fit neatly into 128-bit chunks, you write explicit scalar + cleanup code for the remainder. + + SVE (Scalable Vector Extension) is fundamentally different in that it uses a Vector Length Agnostic (VLA) programming model. Key + differences: + + ┌────────────────┬────────────────────────┬──────────────────────────────────────────────────────────────────┐ + │ │ Neon │ SVE / SVE2 │ + ├────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────────┤ + │ Vector width │ Fixed 128-bit │ Scalable: 128 to 2048 bits (in 128-bit increments) │ + │ Registers │ 32 × 128-bit V regs │ 32 × scalable Z regs + 16 predicate regs │ + │ Tail handling │ Manual scalar cleanup │ Built-in **per-lane predication** (`svwhilelt`, predicate masks) │ + │ Loop model │ Fixed iteration count │ VLA: `svcntb()` at runtime, one binary works on any SVE width │ + │ Gather/scatter │ No │ Yes — `svld1_gather`, `svst1_scatter` │ + │ Dot product │ Limited (Armv8.2 UDOT) │ Full `svdot` family with widening accumulate │ + └────────────────┴────────────────────────┴──────────────────────────────────────────────────────────────────┘ + + The key innovation is predication: every SVE instruction can be masked by a predicate register, so the last iteration of a loop + naturally handles any remainder without scalar cleanup. Your adler32-sve.c uses this pattern directly — svwhilelt_b8 generates the + predicate, and all the svdot/svld1 calls respect it. + + What this means for your Neoverse-V2 + + Your system has SVE2 with 128-bit vectors (Neoverse-V2 implements 128-bit SVE). That's the same width as Neon, but you still + benefit from: + + - Predicated loads/stores — no cleanup loops needed + - Richer instruction set — svdot, svindex, gather/scatter, bitperm, crypto extensions + - SVE2 extras — sve2-aes, sve2-bitperm, sve2-sha3 (all present on your CPU) + - Portability — the same SVE binary runs unmodified on Graviton4 (Neoverse-V2, 128-bit) or future chips with wider vectors + + Since -mcpu=native on your system resolves to armv9-a+sve2-*, your Makefile is already generating SVE2 code. Your adler32-sve.c + uses svdot_u32 and svdot_n_u32 which are SVE dot-product intrinsics — these are well-suited to the Neoverse-V2. + + For deeper dives, Arm has several relevant learning paths: + + - Port Code to Arm SVE (https://learn.arm.com/learning-paths/servers-and-cloud-computing/sve/) + - Learn SVE and SME programming with SIMD Loops (https://learn.arm.com/learning-paths/cross-platform/simd-loops/) + - Accelerate Bitmap Scanning with Neon and SVE + (https://learn.arm.com/learning-paths/servers-and-cloud-computing/bitmap_scan_sve2/) +``` + +The response explains that Neon uses fixed 128 bit vectors, while SVE uses vectors of variable length (a multiple of 128 bits, from 128 to 2048 bits). Neoverse N2 and Neoverse V2 support SVE with 128 bit vectors. Neoverse V1 supports SVE with 256 bit vectors. The key point is that your code doesn't need to know the vector length at compile time. SVE intrinsics handle it at runtime. + +## Predicates and loop tails + +Predicates and loop tails may be new to software developers. + +In fixed-width SIMD like Neon, every lane in a vector always participates in every operation. That works fine when your data length is a multiple of the vector width, but it forces you to write special-case scalar code to handle the leftover elements at the end of a loop. With SVE's variable vector length, you don't even know the vector width at compile time, so that approach breaks down entirely. + +SVE solves this with predicate registers. A predicate is a bitmask with one bit per vector element. Each bit controls whether the corresponding lane is active or inactive for a given operation. Inactive lanes are ignored: they don't load memory, don't compute, and don't write results. This lets you run the same vector code on the final partial chunk of data as on every full chunk before it, there is no special-case tail loop needed. + +### ASK AI: about how predicate registers work + +Ask your assistant: + +```text +Masking with predicate registers seems like a key concept in SVE. How does it work to handle loops when my data length isn't a multiple of the vector length? +``` + +A sample response is: + +```output + SVE predication for loop tails + + SVE has 16 predicate registers (P0–P15) with one bit per byte lane. Each bit says "active" or "inactive." Nearly every SVE + instruction takes a predicate, so inactive lanes are simply skipped — loads don't fault, stores don't write, arithmetic ignores + them. + + The canonical SVE loop + + uint64_t vl = svcntb(); // vector length in bytes (runtime) + for (uint64_t i = 0; i < n; i += vl) { + svbool_t pg = svwhilelt_b8(i, n); // ① generate mask + svuint8_t v = svld1_u8(pg, &data[i]); // ② predicated load + // ... compute with v ... // ③ all ops respect pg + } + + ① `svwhilelt_b8(i, n)` — compares each lane index i, i+1, i+2, ... against n. Returns a predicate with true for every index < n, + false otherwise. + + With vl=16 and n=50: + + i= 0: pg = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ← full vector + i=16: pg = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ← full vector + i=32: pg = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ← full vector + i=48: pg = [1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ← only 2 remaining + + ② Predicated load — svld1_u8(pg, ptr) loads only active lanes. Inactive lanes become zero. It won't fault even if the inactive + addresses are unmapped — this is a hardware guarantee. + + ③ Downstream arithmetic — operations like svdot, svadd, etc. either take the predicate directly or naturally get zeros from the + inactive lanes of the load. No contribution from out-of-bounds data. + + Compare with Neon (fixed 128-bit) + + // Neon: need two separate loops + for (i = 0; i + 16 <= n; i += 16) { + uint8x16_t v = vld1q_u8(&data[i]); // must have 16 valid bytes + // ... compute ... + } + for (; i < n; i++) { // scalar cleanup for remainder + // ... handle tail byte-by-byte ... + } + + With SVE, the same loop body handles both full and partial iterations. No cleanup loop. + + The three predication modes + + SVE intrinsics use a suffix to control what happens to inactive lanes: + + ┌────────┬─────────────────────────┬──────────────────────────────────────────────┐ + │ Suffix │ Inactive lanes │ When to use │ + ├────────┼─────────────────────────┼──────────────────────────────────────────────┤ + │ `_z` │ Zeroed │ Need a clean zero for inactive elements │ + │ `_m` │ Merged (keep old value) │ Conditional updates within a vector │ + │ `_x` │ Don't care (undefined) │ Fastest — when inactive lanes are never read │ + └────────┴─────────────────────────┴──────────────────────────────────────────────┘ + + Key takeaway + + The predicate register turns the loop tail from a code structure problem (separate cleanup path) into a data problem (a mask). One + loop, one code path, any data length, any vector width. +``` + +The response explains that the `svwhilelt_b8(i, n)` intrinsic creates a predicate where element `k` is active if `i + k < n`. This handles the loop tail automatically. When you're near the end of the data, the predicate deactivates the elements that would go out of bounds. + +A typical SVE loop looks like this: + +```c +uint64_t vl = svcntb(); // vector length in bytes, determined at runtime +for (size_t i = 0; i < n; i += vl) { + svbool_t pg = svwhilelt_b8(i, n); // predicate: active lanes where i+k < n + svuint8_t data = svld1_u8(pg, &buf[i]); + // ... process data ... +} +``` + +The loop body runs even for the final partial vector. The predicate ensures only valid bytes are processed. + +## Loading bytes and widening to 32 bit + +For Adler-32, you're loading `uint8_t` bytes but accumulating into `uint32_t` sums. + +### ASK AI: about widening + +Ask your assistant: + +```text +The adler32 loop accumulates uint8_t values into a uint32_t sum. How does SVE handle widening from 8 bit to 32 bit elements? +``` + +A sample output is: + +```output +Loading uint8_t values + + svbool_t pg = svwhilelt_b8(i, len); + svuint8_t bytes = svld1_u8(pg, &data[i]); // predicated load of uint8_t elements + + Widening from 8-bit to 32-bit + + SVE widens one step at a time (8→16→32), and you select the bottom or top half of the source vector at each step: + + // Step 1: 8-bit → 16-bit (unpack lower and upper halves) + svuint16_t lo16 = svunpklo_u16(bytes); // lower half of bytes, zero-extended to u16 + svuint16_t hi16 = svunpkhi_u16(bytes); // upper half of bytes, zero-extended to u16 + + // Step 2: 16-bit → 32-bit (unpack again) + svuint32_t ll32 = svunpklo_u32(lo16); // lower quarter → u32 + svuint32_t lh32 = svunpkhi_u32(lo16); // second quarter → u32 + svuint32_t hl32 = svunpklo_u32(hi16); // third quarter → u32 + svuint32_t hh32 = svunpkhi_u32(hi16); // upper quarter → u32 + + Visually, for a 128-bit vector (16 bytes): + + bytes (16 x u8): [b0 b1 b2 b3 | b4 b5 b6 b7 | b8 b9 b10 b11 | b12 b13 b14 b15] + ↓ ↓ ↓ ↓ + svunpklo_u16 svunpkhi_u16 + ↓ ↓ ↓ ↓ + lo16 (8 x u16): [b0 b1 b2 b3 | b4 b5 b6 b7 ] + hi16 (8 x u16): [b8 b9 b10 b11| b12 b13 b14 b15] + ↓ ↓ ↓ ↓ + ll32 (4 x u32): [b0 b1 b2 b3 ] + lh32 (4 x u32): [b4 b5 b6 b7 ] + hl32 (4 x u32): [b8 b9 b10 b11] + hh32 (4 x u32): [b12 b13 b14 b15] + + Accumulating into a uint32_t sum vector + + Once widened, you add each quarter into your running svuint32_t accumulator: + + svbool_t pg32 = svptrue_b32(); + sum_a = svadd_u32_x(pg32, sum_a, ll32); + sum_a = svadd_u32_x(pg32, sum_a, lh32); + sum_a = svadd_u32_x(pg32, sum_a, hl32); + sum_a = svadd_u32_x(pg32, sum_a, hh32); + + Alternative: `svld1ub_u32` — direct widening load + + If you'd rather avoid the unpack chain, SVE can load bytes directly into 32-bit lanes: + + svbool_t pg32 = svwhilelt_b32(i, len); + svuint32_t vals = svld1ub_u32(pg32, &data[i]); // loads u8, zero-extends to u32 + sum_a = svadd_u32_x(pg32, sum_a, vals); + + The tradeoff: this processes only VL/4 bytes per iteration (4 bytes on your 128-bit Neoverse-V2) instead of VL bytes (16). Simpler + code, but fewer elements per loop iteration. + + Which approach for Adler-32? + + The unpack chain is better for performance — you process a full vector of bytes each iteration. The svld1ub_u32 approach is + simpler but does 4x more loop iterations on your hardware. + + For the b accumulator in Adler-32, you'll also need a prefix sum pattern since b += a after each byte addition — that's the + trickiest part. +``` + +The response introduces `svld1_u8` for loading bytes, and explains that SVE doesn't have a single "load and widen" intrinsic. Instead, you use `svld1ub_u32` to load bytes and extend them with zeroes directly into a 32 bit vector. This is the right approach for Adler-32 because your accumulators are 32 bit. + +The Arm MCP server will return the exact signature: + +```c +svuint32_t svld1ub_u32(svbool_t pg, const uint8_t *base); +``` + +This loads one byte per active 32 bit lane and extends each byte with zeroes to 32 bits. On a processor with 256 bit SVE vectors, this loads 8 bytes per iteration (8 lanes × 32 bits = 256 bits). + +## The dot product intrinsic + +You may see things in the AI assistant responses you don't understand. You can continue to ask for more explanation until you are totally comfortable. One of the responses above implies svdot is commonly used for arithmetic. You can ask more for information about what it does. + +### ASK AI: about the svdot instruction + +Ask your assistant: + +```text +What is the svdot intrinsic? How does it differ from a simple multiply and accumulate operation? +``` + +A sample output is: + +```output +`svdot` — Dot Product Intrinsic + + From the Arm intrinsics reference (https://developer.arm.com/architectures/instruction-sets/intrinsics/#q=svdot[_u32]): + + svuint32_t svdot_u32(svuint32_t acc, svuint8_t op1, svuint8_t op2); + + For each u32 element i in the result: + + acc[i] += (u32)op1[4i+0] * (u32)op2[4i+0] + + (u32)op1[4i+1] * (u32)op2[4i+1] + + (u32)op1[4i+2] * (u32)op2[4i+2] + + (u32)op1[4i+3] * (u32)op2[4i+3] + + It does three things in one instruction: widens u8→u32, multiplies, and accumulates groups of 4. + + How it differs from a simple multiply-accumulate + + A naive approach would be: + + Step 1: widen 16 × u8 → 16 × u32 (svunpklo/hi, twice) + Step 2: multiply 16 × u32 * 16 × u32 (svmul) + Step 3: accumulate into u32 vector (svadd) + + That's 5+ instructions and needs 4× the register space (16 u8 elements expand to 16 u32 elements across multiple vectors). + + svdot instead: + + One instruction: 16 × u8 → 4 × u32 (widened, multiplied, summed, accumulated) + + The key differences: + + ┌───────────────────┬───────────────────────────────┬────────────────────────────────────┐ + │ │ Multiply-accumulate │ svdot │ + ├───────────────────┼───────────────────────────────┼────────────────────────────────────┤ + │ Instructions │ 5+ (unpack, mul, add) │ 1 │ + │ Output elements │ Same count as input │ **4× fewer** (groups of 4 reduced) │ + │ Register pressure │ 4× wider intermediate vectors │ Stays in u32 width │ + │ Widening │ Explicit, multi-step │ Implicit, fused │ + └───────────────────┴───────────────────────────────┴────────────────────────────────────┘ + + The 4:1 reduction is the critical property. On your 128-bit Neoverse-V2, a vector holds 16 u8 bytes. After svdot, you have 4 u32 + lanes — each holding the accumulated sum of 4 bytes. This compression means you process 4× more input data per u32 accumulator + lane than you could with element-wise operations. + + There's also a scalar-broadcast variant: + + svuint32_t svdot_n_u32(svuint32_t acc, svuint8_t op1, uint8_t op2); + + This multiplies every byte by the same scalar value op2. With op2 = 1, it becomes a pure widening sum — no multiplication cost, + just acc[i] += bytes[4i+0] + bytes[4i+1] + bytes[4i+2] + bytes[4i+3]. +``` + +The response explains that `svdot` computes a dot product between two vectors of narrow elements and accumulates the result into a vector of wider elements. For example, `svdot_u32` takes two `svuint8_t` vectors and a `svuint32_t` accumulator, multiplying corresponding 8 bit elements and adding the products into 32 bit lanes. + +The signature is: + +```c +svuint32_t svdot_u32(svuint32_t op1, svuint8_t op2, svuint8_t op3); +``` + +You'll use this to compute the weighted sum for the `B` accumulator. The reason will become clear in the SVE implementation section. + +{{< notice Note >}} +You may notice your AI assistant asking to just create the code for you. Resist the urge to say yes and continue to ask questions and understand the theory of operation. You need to do this to get a functional result with the best performance. You also need to be able to explain and maintain the code so it's worth the extra time to learn how SVE works. +{{< /notice >}} + +## What you've learned and what's next + +In this section: + +1. You learned how SVE predicates handle loop tails without special case code +2. You investigated intrinsics for loading bytes and dot product and learned how to ask for more details +3. You understand that SVE code must be vector length agnostic + +Before you can use these intrinsics effectively, you need to restructure the Adler-32 algorithm to remove the modulo operation on each byte. That's the subject of the next section. diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/4-nmax-optimization.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/4-nmax-optimization.md new file mode 100644 index 0000000000..f84e3733bb --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/4-nmax-optimization.md @@ -0,0 +1,191 @@ +--- +title: Defer the modulo with the NMAX optimization +weight: 5 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Why modulo operations block vectorization + +Look at the original scalar loop again: + +```c +for (size_t i = 0; i < len; i++) { + a = (a + data[i]) % MOD_ADLER; + b = (b + a) % MOD_ADLER; +} +``` + +The `% MOD_ADLER` operation runs on every single byte. Division is expensive, and doing it 10 million times for a 10 MB buffer is a significant cost. More importantly, it prevents vectorization because each iteration depends on the modulo-reduced result of the previous one. + +The standard solution is to defer the modulo. Of course, you might not see this immediately, but may be able to ask a question about optimizing Adler-32. + +### ASK AI: about the cost of modulo operations + +Ask your assistant: + +```text +Are there any common techniques to optimize adler-32 and reduce modulo operations? +``` + +A sample output is: + +```output +The deferred modulo trick + + Instead of % 65521 every byte, you accumulate freely for a batch of bytes, then apply the modulo once at the end of the batch. + This replaces N expensive divisions with N cheap additions + 1 division. + + How many bytes before overflow? + + We need to find the maximum batch size n where neither a nor b can overflow a uint32_t (max value 2³² - 1 = 4,294,967,295). + + For `a`: worst case is every byte = 255, and a starts at its max post-modulo value (65520): + + a_max = 65520 + 255 * n + + For `b`: b accumulates the running sum of a values. Worst case, b starts at 65520 and each a value is as large as possible: + + b_max = 65520 + Σ(65520 + 255*k) for k=1..n + = 65520 + 65520*n + 255*n*(n+1)/2 + + b overflows first because it grows quadratically. Solving for b_max ≤ 2³² - 1: + + 65520 + 65520*n + 255*n*(n+1)/2 ≤ 4,294,967,295 + + This gives n = 5552. + + The standard block size + + 5552 bytes is the universally used batch size in optimized Adler-32 implementations (zlib, chromium, etc.). The loop structure + becomes: + + uint32_t a = 1, b = 0; + + while (len > 0) { + size_t block = (len < 5552) ? len : 5552; + len -= block; + + // Fast inner loop — no modulo here + for (size_t i = 0; i < block; i++) { + a += *data++; + b += a; + } + + // Modulo once per block + a %= MOD_ADLER; + b %= MOD_ADLER; + } + + The payoff + + ┌─────────────────────────────┬──────────────────────────────────┐ + │ Approach │ Modulo operations for 64KB input │ + ├─────────────────────────────┼──────────────────────────────────┤ + │ Per-byte modulo │ 65,536 × 2 = **131,072** │ + │ Deferred (5552-byte blocks) │ 12 × 2 = **24** │ + └─────────────────────────────┴──────────────────────────────────┘ + + That's a ~5,400x reduction in modulo operations. And the inner loop is now just additions — which is exactly what SVE svdot and + svadd can vectorize. +``` + +The response explains the NMAX technique. The key insight is that you can accumulate many bytes before the intermediate values overflow a 32-bit integer, as long as you apply the modulo periodically. + +## Implement the scalar NMAX version + +Before adding SVE, implement the NMAX optimization using scalar values. This is a useful intermediate step: it lets you verify the optimization is correct and measure its impact independently of vectorization. + +Create `adler32-nmax.c`: + +```c +/* + * Adler-32 with NMAX modulo deferral — scalar optimization + */ +#include +#include + +#define MOD_ADLER 65521 +#define NMAX 5552 /* max bytes before b overflows uint32_t */ + +uint32_t adler32(const uint8_t *data, size_t len) +{ + uint32_t a = 1; + uint32_t b = 0; + + while (len > 0) { + size_t n = len < NMAX ? len : NMAX; + len -= n; + + /* inner loop: no modulo */ + for (size_t i = 0; i < n; i++) { + a += data[i]; + b += a; + } + data += n; + + a %= MOD_ADLER; + b %= MOD_ADLER; + } + + return (b << 16) | a; +} +``` + +The structure is now an outer loop that processes NMAX-byte blocks, and an inner loop with no modulo at all. The modulo only runs once per 5552 bytes instead of once per byte. + +## Update the Makefile to test the NMAX version + +Update your `Makefile` to make it easy to switch between implementations: + +```makefile +CC = gcc +CFLAGS = -O3 -mcpu=native -flto -Wall -Wextra +LDFLAGS = -flto +TARGET = adler32-test + +# Change this line to switch implementations: +# IMPL = adler32-simple.c +IMPL = adler32-nmax.c + +$(TARGET): $(IMPL) adler32-test.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(IMPL) adler32-test.c + +run: $(TARGET) + ./$(TARGET) + +clean: + rm -f $(TARGET) + +.PHONY: run clean +``` + +Edit the Makefile to use `adler32-nmax.c` and build and run with the NMAX version: + +```bash +make clean && make run +``` + +The output is similar to: + +```output +Correctness: adler32("Wikipedia") = 0x11E60398 [PASS] + +Performance: + 1 KB 1024 bytes 100000 iters 49.180 ms 1985.7 MB/s checksum=0x37B4063C + 10 KB 10240 bytes 10000 iters 48.925 ms 1996.0 MB/s checksum=0x5FA40D73 + 100 KB 102400 bytes 1000 iters 48.893 ms 1997.4 MB/s checksum=0x2378D74C + 1 MB 1048576 bytes 100 iters 50.012 ms 1999.5 MB/s checksum=0x058B4B08 + 10 MB 10485760 bytes 10 iters 50.097 ms 1996.1 MB/s checksum=0x285FF1B1 +``` + +This is a substantial improvement over the original scalar version, achieved simply by removing the per-byte modulo. Make a note of these numbers as your new intermediate baseline. + +In this section: + +- You learned why deferring the modulo is safe and how to calculate the NMAX bound +- You implemented the scalar NMAX optimization and measured a significant speedup +- You now have a clean inner loop with no modulo which is the right structure for SVE vectorization + +The inner loop of the NMAX version is now a simple accumulation loop. In the next section, you'll vectorize it with SVE intrinsics. diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/5-sve-implementation.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/5-sve-implementation.md new file mode 100644 index 0000000000..b2bc59fde9 --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/5-sve-implementation.md @@ -0,0 +1,301 @@ +--- +title: Vectorize the Adler-32 inner loop with SVE intrinsics +weight: 6 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Why the b accumulator is hard to vectorize + +The NMAX inner loop is now: + +```c +for (size_t i = 0; i < n; i++) { + a += data[i]; + b += a; +} +``` + +Vectorizing the `a` accumulator is straightforward: load a vector of bytes, sum them all, add to `a`. The `b` accumulator is harder. Each byte's contribution to `b` depends on how many bytes come after it in the block. If you process N bytes at once, `data[0]` contributes N times to `b`, `data[1]` contributes N-1 times, and so on. + +Ask your AI assistant to help you think through this. + +### ASK AI: about how to vectorize the loop + +Ask your assistant: + +```text +How can I vectorize the inner loop of the NMAX version using SVE? Provide a detailed explanation for how to do it and teach me about the intrinsics used. +``` + +A sample response is: + +```output +Vectorizing the Adler-32 NMAX Inner Loop with SVE + + Starting Point + + Your scalar NMAX inner loop: + + for (size_t i = 0; i < n; i++) { + a += data[i]; + b += a; + } + + a is a running sum of bytes. b is a running sum of a. The dependency of b on a at every step makes this look serial — but we can + reformulate the math to break that dependency. + + The Mathematical Insight + + Consider a chunk of n bytes: d[0], d[1], ..., d[n-1]. Expanding what b accumulates: + + After d[0]: b += a + d[0] + After d[1]: b += a + d[0] + d[1] + ... + After d[n-1]: b += a + d[0] + d[1] + ... + d[n-1] + + Summing all contributions: + + b_total = n * a_in + (n)*d[0] + (n-1)*d[1] + ... + (1)*d[n-1] + + So b is the sum of two things: + + 1. n * a_in — the initial value of a multiplied by the chunk length + 2. A weighted sum of the data bytes, where byte at position i has weight (n - i) — its distance from the end of the chunk + + And a is simply: + + a_total = a_in + d[0] + d[1] + ... + d[n-1] + + Both the plain sum and the weighted sum can be computed in parallel across vector lanes. The key SVE instruction that makes this + efficient is `svdot` — a dot product that multiplies pairs of uint8_t values, widens to uint32_t, and accumulates, all in one + instruction. + + SVE Implementation Strategy + + Step 1: Build a descending weight vector + + We need weights [n, n-1, n-2, ..., n-vl+1] for the first iteration, then subtract vl each iteration so the weights keep descending + across the full chunk: + + svuint8_t weights = svindex_u8((uint8_t)n, (uint8_t)-1); + + svindex_u8(base, step) creates a vector where lane k = base + k * step. With base = n and step = -1 (which wraps to 255 in uint8, + but the arithmetic works out modulo 256), this gives [n, n-1, n-2, ...]. + + For a chunk of 24 bytes with vl = 8, the weights across three iterations would be: + + Iteration 0: weights = [24, 23, 22, 21, 20, 19, 18, 17] + Iteration 1: weights = [16, 15, 14, 13, 12, 11, 10, 9] + Iteration 2: weights = [ 8, 7, 6, 5, 4, 3, 2, 1] + + Each byte gets multiplied by exactly its distance from the end — matching the formula. + + Step 2: The vectorized loop + + svuint32_t a_vec = svdup_u32(0); + svuint32_t b_vec = svdup_u32(0); + + for (size_t i = 0; i < n; i += vl) { + svbool_t pg = svwhilelt_b8((uint64_t)i, (uint64_t)n); + svuint8_t bytes = svld1_u8(pg, &data[i]); + + b_vec = svdot_u32(b_vec, bytes, weights); + a_vec = svdot_n_u32(a_vec, bytes, 1); + + weights = svsub_n_u8_x(svptrue_b8(), weights, (uint8_t)vl); + } + + Let's walk through each line: + + `svwhilelt_b8(i, n)` — Creates a predicate (bitmask) where lane k is active if i + k < n. For full iterations this is all-true. + For the final iteration, it masks off lanes past the end of the data. This eliminates the need for a scalar tail loop — SVE's + predication handles the remainder natively. + + `svld1_u8(pg, &data[i])` — Predicated load of bytes. Active lanes load from memory; inactive lanes produce zero. This is safe even + if data + i would read past the buffer for inactive lanes. + + `svdot_u32(b_vec, bytes, weights)` — This is the powerhouse. It takes two uint8_t vectors, multiplies corresponding elements, + groups the products in sets of 4, sums each group with widening to uint32_t, and accumulates into b_vec. Visually for one group of + 4: + + b_vec[k] += bytes[4k]*weights[4k] + bytes[4k+1]*weights[4k+1] + + bytes[4k+2]*weights[4k+2] + bytes[4k+3]*weights[4k+3] + + This computes the weighted sum for b directly — each byte multiplied by its distance-from-end weight. + + `svdot_n_u32(a_vec, bytes, 1)` — Same dot product, but the second operand is the scalar 1 broadcast to all lanes. This effectively + becomes a widening sum: each group of 4 bytes is summed and accumulated into a uint32_t lane. It computes the simple byte sum for + a. + + `svsub_n_u8_x(svptrue_b8(), weights, vl)` — Subtract vl from every weight lane. After processing vl bytes, the next batch of bytes + is vl positions closer to the end, so their weights are vl less. The _x suffix means "don't care about inactive lanes" — fastest + form. + + Step 3: Horizontal reduction and scalar fixup + + b += a * (uint32_t)n; + a += (uint32_t)svaddv_u32(svptrue_b32(), a_vec); + b += (uint32_t)svaddv_u32(svptrue_b32(), b_vec); + + a %= MOD_ADLER; + b %= MOD_ADLER; + + `svaddv_u32(pred, vec)` — Horizontal reduction: sums all active uint32_t lanes into a single scalar. We use this to collapse both + vector accumulators back to scalars. + + The b += a * n line accounts for the n * a_in term from our formula — the initial value of a contributed to b at every byte + position, but the vector loop only computed the weighted data sum. + + Note the ordering: b is updated with a * n before a is modified, because we need the original a_in value. +``` + +The response introduces `svindex_u32`, which creates a vector with a starting value and a step: + +```c +// Creates [N, N-1, N-2, ..., N-vl+1] where vl is the vector length +svuint32_t weights = svindex_u32((uint32_t)vl, (uint32_t)-1); +``` + +Combined with `svdot`, this computes the weighted sum in a single instruction. The `svdot_u32` intrinsic multiplies corresponding 8-bit elements from two vectors and accumulates the products into 32-bit lanes: + +```c +// b_vec += data_bytes * weights (dot product, 4 bytes per 32-bit lane) +b_vec = svdot_u32(b_vec, data_u8, weights_u8); +``` + +This step can be confusing at first. The key is that `svdot` works on groups of four 8-bit elements per 32-bit lane. You need to prepare the weight vector in 8-bit form to match. + +This is the skeleton. The full implementation requires careful handling of the weight vector for partial vectors at the loop tail. + +You can continue learning by asking questions and coding. You can also use your AI assistant to check your code and explain it. + +It's unlikely that just asking your assistant to write the code using SVE intrinsics will function correctly with best performance. + +## The complete SVE implementation + +Create `adler32-sve.c`. This is the full implementation based on the concepts you've learned: + +```c +/* + * Adler-32 with SVE vectorization and NMAX modulo deferral + */ +#include +#include +#include + +#define MOD_ADLER 65521 +#define NMAX 5552 + +uint32_t adler32(const uint8_t *data, size_t len) +{ + uint32_t a = 1; + uint32_t b = 0; + + const uint64_t vl = svcntb(); + + while (len > 0) { + size_t n = len < NMAX ? len : NMAX; + len -= n; + + svuint32_t a_vec = svdup_u32(0); + svuint32_t b_vec = svdup_u32(0); + + /* + * Descending weights in u8: [n, n-1, n-2, ..., n-vl+1]. + * Byte at position i within the block gets weight (n - i), + * matching how many times it contributes to b. + */ + svuint8_t weights = svindex_u8((uint8_t)n, (uint8_t)-1); + + for (size_t i = 0; i < n; i += vl) { + svbool_t pg = svwhilelt_b8((uint64_t)i, (uint64_t)n); + svuint8_t bytes = svld1_u8(pg, &data[i]); + + /* b += data[i]*w[0] + data[i+1]*w[1] + ... (weighted sum) */ + b_vec = svdot_u32(b_vec, bytes, weights); + + /* a += data[i] + data[i+1] + ... (simple sum via dot with 1) */ + a_vec = svdot_n_u32(a_vec, bytes, 1); + + /* Decrease all weights by vl for next iteration */ + weights = svsub_n_u8_x(svptrue_b8(), weights, (uint8_t)vl); + } + + data += n; + + /* + * a_vec lanes each hold partial sums of groups of 4 bytes. + * b_vec lanes hold partial weighted sums. + * The existing 'a' contributed to b for each of the n bytes. + */ + b += a * (uint32_t)n; + a += (uint32_t)svaddv_u32(svptrue_b32(), a_vec); + b += (uint32_t)svaddv_u32(svptrue_b32(), b_vec); + + a %= MOD_ADLER; + b %= MOD_ADLER; + } + + return (b << 16) | a; +} +``` + +## Update the Makefile for SVE + +Update your `Makefile` to compile the SVE version with the required flags: + +```makefile +CC = gcc +CFLAGS = -O3 -mcpu=native -flto -Wall -Wextra +LDFLAGS = -flto +TARGET = adler32-test + +# Change this line to switch implementations: +#IMPL = adler32-simple.c +#IMPL = adler32-nmax.c +IMPL = adler32-sve.c + +$(TARGET): $(IMPL) adler32-test.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(IMPL) adler32-test.c + +run: $(TARGET) + ./$(TARGET) + +clean: + rm -f $(TARGET) + +.PHONY: run clean +``` + +## Build and verify correctness + +Build and run the correctness test first: + +```bash +make clean && make run +``` + +The expected output starts with: + +```output +Correctness: adler32("Wikipedia") = 0x11E60398 [PASS] + +Performance: + 1 KB 1024 bytes 100000 iters 4.086 ms 23897.6 MB/s checksum=0xFF69063C + 10 KB 10240 bytes 10000 iters 4.439 ms 21997.2 MB/s checksum=0x34F60D73 + 100 KB 102400 bytes 1000 iters 4.436 ms 22015.3 MB/s checksum=0xAE3DD74C + 1 MB 1048576 bytes 100 iters 4.719 ms 21192.0 MB/s checksum=0xB0C34B08 + 10 MB 10485760 bytes 10 iters 4.743 ms 21084.6 MB/s checksum=0x649EF1B1 +``` + +## What you've learned and what's next + +In this section: + +- You learned how to use `svindex_u32` to create position-weight vectors +- You used `svdot` to compute the weighted sum for the `b` accumulator +- You built a complete, vector-length-agnostic SVE implementation + +In the final section, you'll benchmark the SVE version against the scalar and NMAX baselines, and look at the generated assembly to understand what the CPU is actually executing. diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/6-results.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/6-results.md new file mode 100644 index 0000000000..2114001fb8 --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/6-results.md @@ -0,0 +1,87 @@ +--- +title: Benchmark and analyze the results +weight: 7 + +### FIXED, DO NOT MODIFY +layout: learningpathall +--- + +## Compare the results + +Your numbers will vary by processor. Compare against the two baselines you recorded earlier: + +| Implementation | Approx. throughput | Speedup vs. scalar | +|---|---|---| +| Scalar (original) | ~380 MB/s | 1x | +| Scalar NMAX | ~2,000 MB/s | ~5x | +| SVE | ~21,000 MB/s | ~55x | + +The SVE version is roughly 10x faster than the NMAX scalar version, and about 55x faster than the original. The exact ratio depends on your SVE vector length. You can also use a Graviton3 instance to try on a processor with 256-bit SVE vectors and compare the results. The 256-bit vector length on Graviton3 shows faster performance than the 128-bit vector length on Graviton4, but Graviton3 is slower than Graviton4 on the scalar versions. + +## Ask about the assembly + +Understanding the generated assembly helps you verify that the compiler is producing the instructions you expect. + +### ASK AI: about the inner loop assembly code + +Ask your assistant: + +```text +disassemble ~/adler32-sve/adler32-test and explain the assembly code for the inner loop. +``` + +The response explains the mapping of the C code to the assembly instructions, explains the intrinsics used. + +A partial example response is: + +```output +Summary + + The entire inner loop is just 7 instructions per vector-width of bytes: + + ┌──────────────────┬───────────────────────────────────────┐ + │ Instruction │ Purpose │ + ├──────────────────┼───────────────────────────────────────┤ + │ `whilelo` │ Generate predicate for this iteration │ + │ `ld1b` │ Predicated load of bytes │ + │ `add` (scalar) │ Advance loop counter │ + │ `udot` (weights) │ Weighted sum for `b` │ + │ `udot` (ones) │ Simple sum for `a` │ + │ `add` (vector) │ Decrement weights │ + │ `cmp` + `b.hi` │ Loop control │ + └──────────────────┴───────────────────────────────────────┘ +``` + +To see the actual assembly, disassemble the binary: + +```bash +objdump -d adler32-test | grep -A 40 "" +``` + +Look for the `WHILELT` and `UDOT` instructions in the inner loop. If you see them, the SVE code path is active. + +## Ask about debugging and performance tuning + +You can also use your AI assistant to debug any issues or clarify performance, but be careful, it is easy to divert into an endless loop of trial and error as today's assistants can easily make things worse. + +## What you've accomplished + +You've completed the full optimization journey for Adler-32 on Arm Neoverse using an AI Assistant and the Arm MCP server: + +1. You started with a simple scalar implementation and measured its baseline performance +2. You used the Arm MCP server to learn SVE concepts such as predicates, widening loads, dot products, and reductions without looking up documentation +3. You applied the NMAX modulo-deferral technique to restructure the algorithm for vectorization +4. You built a vector-length-agnostic SVE implementation that works correctly on any SVE-capable processor +5. You measured a performance improvement and learned how to read the generated assembly + +## Apply this process to your own code + +The process you followed here applies directly to other scalar loops in your own projects: + +1. Establish a correctness test and a performance baseline before changing anything +2. Ask your AI assistant to guide you and keep explaining along the way +3. Use the Arm MCP server to look up the specific intrinsics you need, one concept at a time +4. Validate correctness before measuring performance +5. Compare against each intermediate version to understand where the speedup comes from + +The Arm MCP server's intrinsics reference covers all SVE and SVE2 intrinsics. As you encounter more complex loops, you can use the same question-and-answer approach to find the right intrinsics for your specific data types and operations. diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/_index.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/_index.md new file mode 100644 index 0000000000..2e16bdc5c6 --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/_index.md @@ -0,0 +1,75 @@ +--- +title: Optimize an Adler-32 checksum function with SVE intrinsics using the Arm MCP server + +draft: true +cascade: + draft: true + +description: Use the Arm MCP server with an AI coding assistant to incrementally optimize a scalar C Adler-32 checksum function using SVE intrinsics on Arm Neoverse servers. + +minutes_to_complete: 45 + +who_is_this_for: This is an introductory topic for C/C++ developers who want to learn how to vectorize code using Arm SVE intrinsics, guided by an AI coding assistant connected to the Arm MCP server. + +learning_objectives: + - Optimize C code by learning from an AI assistant + - Establish a reproducible performance baseline for a scalar Adler-32 implementation written in C + - Apply the NMAX technique to defer modulo operations and improve scalar throughput + - Implement an SVE version of Adler-32 using svwhilelt, svdot, and svaddv + - Validate correctness and measure the performance improvement of the SVE implementation + +prerequisites: + - An AI coding assistant configured with the Arm MCP server, such as Kiro CLI, GitHub Copilot, or Gemini CLI. See the [Arm MCP server Learning Path](/learning-paths/servers-and-cloud-computing/arm-mcp-server/) for setup instructions. + - An Arm Neoverse server running Ubuntu 26.04 with SVE support (for example, AWS Graviton3 or later, Google Axion, or Microsoft Cobalt 100) + - Basic familiarity with C programming + +author: Jason Andrews + +skilllevels: Introductory +subjects: Performance and Architecture +cloud_service_providers: + - AWS + - Microsoft Azure + - Google Cloud + +armips: + - Neoverse + +operatingsystems: + - Linux + +tools_software_languages: + - C + - GCC + - SVE + - MCP + +further_reading: + - resource: + title: Arm MCP server Learning Path + link: /learning-paths/servers-and-cloud-computing/arm-mcp-server/ + type: learning-path + - resource: + title: Arm intrinsics reference + link: https://developer.arm.com/architectures/instruction-sets/intrinsics/ + type: website + - resource: + title: Adler-32 checksum algorithm + link: https://en.wikipedia.org/wiki/Adler-32 + type: website + - resource: + title: SVE programming examples + link: /learning-paths/servers-and-cloud-computing/sve/ + type: learning-path + - resource: + title: Arm C Language Extensions for SVE + link: https://developer.arm.com/documentation/100987/latest/ + type: documentation + + +### FIXED, DO NOT MODIFY +# ================================================================================ +weight: 1 # _index.md always has weight of 1 to order correctly +layout: "learningpathall" # All files under learning paths have this same wrapper +learning_path_main_page: "yes" # This should be surfaced when looking for related content. Only set for _index.md of learning path content. +--- diff --git a/content/learning-paths/servers-and-cloud-computing/adler32-kiro/_next-steps.md b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/_next-steps.md new file mode 100644 index 0000000000..c3db0de5a2 --- /dev/null +++ b/content/learning-paths/servers-and-cloud-computing/adler32-kiro/_next-steps.md @@ -0,0 +1,8 @@ +--- +# ================================================================================ +# FIXED, DO NOT MODIFY THIS FILE +# ================================================================================ +weight: 21 # Set to always be larger than the content in this path to be at the end of the navigation. +title: "Next Steps" # Always the same, html page title. +layout: "learningpathall" # All files under learning paths have this same wrapper for Hugo processing. +---