Showing posts with label GPU. Show all posts
Showing posts with label GPU. Show all posts

5/06/2026

AMD GPU Programming Primer — Threads, Waves, Tiles & Vector Loads

AMD GPU Programming Primer

Threads · Waves · Memory · Tile Distribution · Vector Loads · MFMA

1. The execution hierarchy: grid → workgroup → wave → thread

A GPU kernel launch is a hierarchy of work units. Bigger units contain smaller ones.

AMD termNVIDIA termWhat it is
GridGridThe whole kernel launch — covers the entire problem.
WorkgroupBlock / threadblockA group of threads on one Compute Unit (CU). Shares LDS (shared memory). Can synchronize via __syncthreads().
Wavefront (wave)Warp64 threads (AMD CDNA) executing the same instruction simultaneously (SIMT).
Thread (work-item)ThreadOne lane in a wave. Has its own thread ID and register state.
GRID (kernel launch — covers the whole problem)
Workgroup 0 (256 threads)
Wave 0 (T0..T63)
Wave 1 (T64..T127)
Wave 2 (T128..T191)
Wave 3 (T192..T255)
Workgroup 1 (256 threads)
Wave 0..3 (64 threads each)
Workgroup N−1
Wave 0..3
Key: 64 threads in a wave always execute the same instruction in lock-step. That is the essence of SIMT (Single Instruction Multiple Threads).

2. Lane vs thread

"Lane" and "thread" are two views of the same physical execution slot.

  • Lane = a hardware ALU slot inside a SIMD unit. There are exactly 64 lanes per SIMD on AMD CDNA.
  • Thread = the software view of one lane. Has its own thread ID and private registers.

One lane runs one thread at a time. They are 1:1 within an executing wave.

1 wave (= 64 threads) running on 1 SIMD:

   Lane 0  ↔  Thread 0    (running my_function with tid=0)
   Lane 1  ↔  Thread 1    (running my_function with tid=1)
   Lane 2  ↔  Thread 2
    ...
   Lane 63 ↔  Thread 63

All 64 lanes execute the same instruction at the same cycle.

3. Hardware: GPU → CU → SIMD → lane

Below the software hierarchy is the physical hardware:

  • GPU contains many CUs (Compute Units). Example: MI300X has 304 CUs.
  • CU contains 4 SIMD units. The 4 SIMDs in a CU operate in parallel.
  • SIMD contains 64 lanes (ALUs) and a register file that can hold up to 8 resident waves.
GPU
├─ CU 0
│   ├─ SIMD 0  (64 lanes, ≤ 8 resident waves)
│   ├─ SIMD 1  (64 lanes, ≤ 8 resident waves)
│   ├─ SIMD 2  (64 lanes, ≤ 8 resident waves)
│   ├─ SIMD 3  (64 lanes, ≤ 8 resident waves)
│   └─ LDS (shared memory, 64 KB)
├─ CU 1
├─ ...
└─ CU 303      ← total 304 CUs on MI300X
SIMD ≠ instant execution. A SIMD holds up to 8 waves in its register file but executes only one wave per cycle. With multiple waves resident, when one wave waits for memory, the SIMD switches to another. This is latency hiding.

4. Registers, VGPRs & occupancy

Register types

TypeSizeScopeNotes
VGPR (vector GPR)32-bit (4 B)Private per laneUp to 256 per lane per wave. Each lane sees its own VGPR.
SGPR (scalar GPR)32-bit (4 B)Shared by 64 lanesUsed for scalar values like loop counters, addresses.
AGPR (accumulator GPR)32-bit (4 B)Private per laneCDNA-only. Used as MFMA accumulators.

How much register memory does one lane have?

1 lane × 256 VGPRs × 4 bytes = 1 KB per lane
1 wave (64 lanes) × 1 KB = 64 KB total register file used by one wave

Occupancy

Occupancy = number of waves resident on a SIMD (1 to 8). Higher occupancy enables better latency hiding.

If a wave uses 256 VGPRs/lane → only 1 wave fits in SIMD → occupancy 1
If a wave uses 128 VGPRs/lane → 2 waves fit                → occupancy 2
If a wave uses  32 VGPRs/lane → 8 waves fit                → occupancy 8 (max)

More resident waves = SIMD can switch when one wave stalls on memory.
Trade-off: using more VGPRs per thread means each thread can hold more data, but fewer waves can be resident, reducing latency hiding.

5. Memory hierarchy: registers → LDS → cache → HBM

GPU memory has multiple levels, similar to CPU cache hierarchy:

LevelSize (per CU / total)Latency (cycles)Managed byCPU analogue
Registers (VGPR/SGPR)~256 KB / CU~1CompilerCPU registers
LDS (shared memory)64 KB / CU~10–30Software (explicit loads/stores)Scratchpad / fast SRAM
L1 cache16 KB / CU~30Hardware (transparent)L1 cache
L2 cache~16 MB total~150HardwareL2 cache
Infinity / L3~256 MB~300HardwareL3 cache
HBM (global memory)192 GB~500–1000HW + softwareDRAM
Key insight: registers are basically free (~1 cycle), HBM is very expensive (~500+ cycles). Performance comes from staging data through LDS and registers, and from hiding HBM latency with high occupancy.

6. Kernel launch <<< grid, block >>>

HIP/CUDA kernel launch syntax:

add_kernel<<< grid, block >>>(A, B, C, N);
ParameterMeaningExample
block (a.k.a. blockSize)Threads per workgroup256 → 4 waves per workgroup
gridNumber of workgroups4 → 4 workgroups total
(implicit)Wave count = blockSize / 64256 / 64 = 4 waves per workgroup

You don't pick the wave count directly — it is derived from blockSize. The hardware always groups threads into waves of 64 on CDNA.

add_kernel<<< grid=4, block=256 >>>(...)

Total threads = 4 × 256 = 1024
Total waves   = 1024 / 64 = 16
Total workgroups = 4

Each workgroup → one CU
Each wave → one SIMD inside that CU

7. Vector loads & the 16-byte rule

A single load instruction can pull up to 16 bytes into a thread's registers. This is the hardware limit on AMD CDNA.

The number of elements per load (called vec size) depends on the data type:

Data typeSize (B)vec=1vec=2vec=4vec=8vec=16
fp16 / bf1622 B4 B8 B16 B (max)
fp3244 B8 B16 B (max)
int811 B2 B4 B8 B16 B (max)
Rule: vec × sizeof(dtype) ≤ 16 byte. Larger vec means fewer load instructions to move the same amount of data — faster.

Example GPU instructions

fp16, vec=1 (2 B):
   global_load_ushort  v0,    v[1:2]    ; load 2 bytes (1 fp16)
fp16, vec=4 (8 B):
   global_load_dwordx2 v[0:1], v[2:3]   ; load 8 bytes (4 fp16)
fp16, vec=8 (16 B):
   global_load_dwordx4 v[0:3], v[4:5]   ; load 16 bytes (8 fp16) - MAX
fp32, vec=4 (16 B):
   global_load_dwordx4 v[0:3], v[4:5]   ; load 16 bytes (4 fp32) - MAX

8. Walking through a simple kernel

__global__ void add_kernel(float* A, float* B, float* C, int N) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;  // global thread ID
    if (tid < N) {
        float a = A[tid];        // load (HBM → register)
        float b = B[tid];        // load (HBM → register)
        float c = a + b;         // ALU (register-to-register, ~1 cycle)
        C[tid] = c;              // store (register → HBM)
    }
}

add_kernel<<< 4, 256 >>>(A, B, C, 1024);

What happens per cycle (assuming occupancy 1, the worst case):

cycle:  1     2..99    100   101..199   200    201
        ----  ------   ----  --------   ----   -----
inst:   load   wait    load    wait     add    store
        A     (idle)   B      (idle)
                                                  ↓
                                          ALU only busy 2 cycles out of 200.

Instructions in this kernel: 4. Actual cycles: ~200. The reason: each HBM load takes ~100 cycles to complete, even though issuing it takes 1 cycle. With occupancy 1, the SIMD has nothing else to do but wait.

If occupancy were 4, the SIMD would switch to other waves during the wait, keeping the ALU busy on every cycle. This is why high occupancy matters.

9. MFMA: cooperative matrix multiply

MFMA (Matrix Fused Multiply-Add) instructions are wave-cooperative: all 64 lanes work together to compute a small matrix multiply (e.g., 16×16).

v_mfma_f32_16x16x16_f16   acc, a_frag, b_frag, c_frag

  64 lanes cooperatively compute D = A × B + C
  where A is 16×16 fp16, B is 16×16 fp16, D is 16×16 fp32

  Each lane holds a small piece of A, B, and accumulates a small piece of D.
  The hardware exchanges data between lanes during execution.

  Latency: ~8–32 cycles (NOT 1 cycle), but throughput is enormous:
    16×16×16 = 4,096 multiply-adds per instruction per wave
Important: MFMA is a wave-level instruction. It cannot be split across waves — one wave executes one MFMA. To compute a larger matrix multiply, multiple waves issue multiple MFMAs (covering different tiles).

CDNA vs RDNA

  • CDNA (data-center: gfx90a, gfx942, gfx950): MFMA available.
  • RDNA (consumer: gfx11, gfx12): no MFMA. Has WMMA (Wave Matrix Multiply-Accumulate) instead with similar idea.

10. Tiles & how a wave fills a tile (X0, Y0, X1, Y1)

What is a tile?

A tile is a 2D chunk of a matrix that one workgroup (or one wave) processes. GPU kernels divide a big problem into many small tiles.

Big matrix (e.g., 1024 × 1024)
divided into tiles of 64 × 64:

       X →
       ┌────────────────────────┐
   Y   │ t0  t1  t2  ...  t15   │
   ↓   │ t16 t17 ...            │   16 × 16 = 256 tiles
       │ ...                    │   each handled by one workgroup
       │ t240 ...          t255 │
       └────────────────────────┘

How does a 64-thread wave fill a 64×64 tile?

One 64×64 tile = 4,096 elements. One wave = 64 threads. Each thread is responsible for 4096/64 = 64 elements.

Those 64 elements per thread are split between two axes:

SymbolMeaning
X1 (= vec)Number of elements one thread loads in one instruction (X direction).
X0Number of threads placed along the X axis.
Y0Number of threads placed along the Y axis.
Y1Number of times each thread iterates along the Y axis.

Constraints:

X0 × Y0     = 64           ← total threads (wave size)
X0 × X1     = XPerTile     ← X axis fully covered
Y0 × Y1     = YPerTile     ← Y axis fully covered (with iteration)
X1 × sizeof ≤ 16 byte      ← hardware load limit

Why the X axis can use vec loads

Memory is 1D, but we view it as 2D (row-major):

memory:  [a][b][c][d] [e][f][g][h] [i][j][k][l] [m][n][o][p]
         ─────────── ─────────── ─────────── ───────────
            row 0       row 1       row 2       row 3

X direction: addresses +1 (contiguous)  → one instruction can load 4/8/16 bytes
Y direction: addresses +width (strided) → needs separate instructions per row

Worked example: 64×64 tile, fp16, vec=4

X1 = 4 (vec)
X0 = XPerTile / X1 = 64 / 4 = 16
Y0 = wave / X0     = 64 / 16 = 4
Y1 = YPerTile / Y0 = 64 / 4  = 16

Per thread:    X1 × Y1 = 4 × 16 = 64 elements
Per wave:      64 threads × 64 elements = 4,096 elements ✓
Load count:    16 (per thread) → 1024 total vec loads → 4,096 elements
                X axis (16 threads × 4 vec = 64 cols)
              ┌─────────────────────────────────────┐
   row 0..3   │ T0  T1  T2  T3  T4  T5 ... T14 T15  │   ← Y0=0, Y1=0..3
   row 4..7   │ T0  T1  T2  T3  ...                 │   ← Y0=0, Y1=4..7
   ...                                                  Y1 iterates 16 times
   row 16..   │ T16 T17 ...                         │   ← Y0=1
   row 32..   │ T32 T33 ...                         │   ← Y0=2
   row 48..63 │ T48 T49 ... T63                     │   ← Y0=3
              └─────────────────────────────────────┘

Choosing vec size

vecX0Y0Y1Loads / threadQuality
16416464worst (no vec)
23223232poor
41641616good
88888best (fp16 max)
Larger vec → fewer load instructions → faster, up to the 16-byte hardware limit.

11. Tile distribution patterns: thread / warp / block raked

The same tile can be distributed across threads in several ways. The choice depends on the algorithm, the data layout, and the matrix instruction (MFMA) shape.

PatternWho covers one tileWave layout inside tile
thread_raked1 wave (64 threads)n/a (single wave)
warp_rakedMultiple waves cooperate1D stack (waves stripe along one axis)
block_rakedAll waves of the workgroup2D grid (waves arranged in a grid)

warp_raked — 1D wave layout (4 waves stacked along Y)

                X axis (XPerTile)
              ┌─────────────────────────────────┐
              │          Wave 0                 │   each wave covers full X width
              ├─────────────────────────────────┤
              │          Wave 1                 │   1/4 of Y
              ├─────────────────────────────────┤
              │          Wave 2                 │
              ├─────────────────────────────────┤
              │          Wave 3                 │
              └─────────────────────────────────┘

block_raked — 2D wave layout (4 waves in 2×2 grid)

                X axis
              ┌───────────────┬─────────────┐
              │  Wave 0       │  Wave 1     │
              │ (X 0..63)     │ (X 64..127) │
              ├───────────────┼─────────────┤
              │  Wave 2       │  Wave 3     │
              │ (X 0..63)     │ (X 64..127) │
              └───────────────┴─────────────┘
warp_raked (1D)block_raked (2D)
Wave layout1 axis (Y)2 axes (X × Y)
Sub-tile per waveXPerTile × (YPerTile/N)(XPerTile/M) × (YPerTile/M)
X coverage by one wavefullpartial
The choice of pattern affects memory access patterns, MFMA fragment alignment, and register tile shapes. Each is best suited to different scenarios.

12. Cheat sheet

ConceptDefinition
GridThe whole kernel launch.
Workgroup (block)Group of threads on one CU. Shares LDS.
Wave (warp)64 threads (CDNA) executing in lockstep (SIMT).
Thread (work-item)One software unit; runs on one lane.
LaneOne hardware ALU slot in a SIMD. 64 per SIMD.
SIMDHardware execution unit. 4 per CU. Can hold up to 8 resident waves.
CUCompute Unit. Contains 4 SIMDs and 64 KB LDS. ~304 per MI300X.
VGPRVector register, private per lane. 32-bit. Up to 256 per lane.
SGPRScalar register, shared by all 64 lanes in a wave.
AGPRAccumulator register (CDNA), used for MFMA output.
LDSLocal Data Share = software-managed shared memory. 64 KB per CU.
HBMGlobal memory. Large but slow (~500+ cycle latency).
OccupancyNumber of resident waves on a SIMD (1–8). Higher = better latency hiding.
Latency hidingSIMD switches to another resident wave while one waits on memory.
SIMTSingle Instruction Multiple Threads. All 64 lanes run the same instruction.
MFMAMatrix Fused Multiply-Add. Wave-cooperative matrix multiply (CDNA).
WMMARDNA equivalent of MFMA.
Tile2D chunk of a matrix processed by one workgroup.
XPerTile / YPerTileTile dimensions in elements (algorithm-defined).
vec / X1Elements per thread per load. Constrained by 16-byte limit.
X0, Y0Number of threads placed along X / Y axes.
Y1Y-axis iteration count per thread.
CoalescingAdjacent threads accessing adjacent memory → one wide HBM transaction.
Kernel launchkernel<<< grid, block >>>(...) — grid = workgroups, block = threads/wg.
tile_distribution_patternHow threads/waves are distributed across one tile (thread/warp/block raked).
Key takeaways
  • Threads run in waves of 64; one instruction = one wave-step.
  • Memory is the slow part. High occupancy hides memory latency.
  • Vector loads (up to 16 B) reduce instruction count dramatically.
  • Tile dimensions are chosen by the algorithm; thread layout is derived from them.
  • MFMA is wave-cooperative — cannot be split across waves.

12/30/2025

MareArts ANPR V14 Models - Complete Performance Guide & Benchmarks


⚡ MareArts ANPR V14 Models - Performance, Metrics & How to Choose

Choosing the right ANPR model is crucial for your application. Too heavy? Slow performance. Too light? Lower accuracy. In this comprehensive guide, we'll break down all MareArts ANPR V14 models with real benchmarks to help you make the perfect choice.

🎯 Two-Stage Pipeline Architecture

MareArts ANPR uses a two-stage pipeline:

  1. Detector - Finds license plates in images (Where is the plate?)
  2. OCR - Reads text from detected plates (What does it say?)

You can mix and match models from each stage to optimize for your specific needs!

📊 Detector Models - Find License Plates

Model Sizes Explained

Size Parameters Speed Accuracy Best For
pico Smallest Fast Good (96-98%) Mobile, Edge devices
micro Small Very Fast Excellent (97-99%) 🏆 Best overall
small Medium Fastest Excellent (98-99%) High-speed applications
medium Large Fast Excellent (98-99%) Balanced
large Largest Moderate Highest (99%+) Maximum accuracy

Resolution Options

  • 320p models (320×320) - 2× faster, 96-98% detection
  • 640p models (640×640) - Highest accuracy, 98-99% detection

Precision Options

  • FP32 - Fastest on GPU (2× faster than FP16), standard size
  • FP16 - 50% smaller file size, same accuracy, slower inference

Complete Detector Performance Table

Model Name Detection Rate Speed (GPU) Size Recommendation
micro_320p_fp32 97.13% 128 FPS (7.8ms) 83 MB 🏆 Best overall
micro_320p_fp16 97.13% 56 FPS (17.9ms) 42 MB 🏆 Best mobile
small_320p_fp32 98.00% 142 FPS (7.0ms) 114 MB ⚡ Fastest
medium_320p_fp32 98.06% 136 FPS (7.4ms) 153 MB High detection
large_320p_fp32 98.40% 131 FPS (7.6ms) 164 MB Strong performance
pico_320p_fp32 96.02% 129 FPS (7.8ms) 75 MB 📱 Smallest + fast
pico_640p_fp32 98.54% 66 FPS (15.2ms) 75 MB Balanced
small_640p_fp32 99.15% 70 FPS (14.3ms) 114 MB High detection
medium_640p_fp32 99.21% 66 FPS (15.1ms) 153 MB Very high
large_640p_fp32 99.31% 60 FPS (16.7ms) 164 MB 🎯 Highest accuracy

Key Findings:

  • 320p models: 2× faster than 640p (96-98% accuracy)
  • 640p models: Highest accuracy (98-99%) for difficult cases
  • FP16 models: 50% smaller, same accuracy, ~50% slower
  • Recommended: micro_320p_fp32 (best speed/accuracy balance)

📖 OCR Models - Read License Plate Text

Two Key Metrics

  • Exact Match - Entire plate number is 100% correct
  • Character Accuracy - Percentage of individual characters correct

Example: Actual plate: "ABC-1234"

  • OCR reads "ABC-1234" → ✅ Exact Match = Yes, Char Accuracy = 100%
  • OCR reads "ABC-1235" → ❌ Exact Match = No, Char Accuracy = 87.5% (7/8 correct)

Complete OCR Performance by Region

🌍 Universal (univ) - All Regions
Model Exact Match Char Accuracy FPS Size
pico_fp32 97.48% 98.87% 264 20 MB
micro_fp32 97.54% 98.86% 260 71 MB
small_fp32 97.51% 98.85% 291 112 MB
medium_fp32 97.57% 98.89% 245 164 MB
large_fp32 97.75% 98.91% 253 179 MB
🇰🇷 Korean (kr) - Best Overall Accuracy
Model Exact Match Char Accuracy FPS
pico_fp32 98.99% 99.77% 272
micro_fp32 99.21% 99.80% 250
small_fp32 99.19% 99.80% 295
medium_fp32 99.21% 99.80% 267
large_fp32 99.27% 99.82% 265
🇪🇺 Europe+ (eup) - EU + Additional Countries
Model Exact Match Char Accuracy FPS
pico_fp32 94.98% 97.39% 280
micro_fp32 95.07% 97.46% 266
small_fp32 94.98% 97.43% 304
medium_fp32 95.03% 97.46% 278
large_fp32 95.32% 97.54% 260
🇺🇸 North America (na) - USA, Canada, Mexico
Model Exact Match Char Accuracy FPS
pico_fp32 71.21% 88.43% 268
micro_fp32 71.21% 87.67% 269
small_fp32 69.70% 88.27% 311
medium_fp32 63.64% 87.24% 284
large_fp32 69.70% 86.25% 271
🇨🇳 China (cn)
Model Exact Match Char Accuracy FPS
pico_fp32 96.24% 98.82% 268
micro_fp32 96.30% 98.74% 265
small_fp32 96.36% 98.88% 301
medium_fp32 96.36% 98.89% 276
large_fp32 96.49% 98.87% 262

OCR Model Averages (All Regions)

Model Avg Exact Match Avg Char Accuracy Avg FPS Size
small_fp32 91.54% 96.64% 300 FPS 112 MB
pico_fp32 91.78% 96.65% 270 FPS 20 MB
micro_fp32 91.86% 96.50% 262 FPS 71 MB
medium_fp32 90.36% 96.45% 270 FPS 164 MB
large_fp32 91.70% 96.27% 262 FPS 179 MB

🌍 Regional Vocabulary Support

Region Code Coverage Character Sets
Universal univ All regions (default) All character sets
Korea kr South Korea Hangul + Latin + Digits
Europe+ eup EU + UK, Switzerland, Norway Latin + Cyrillic + Special
North America na USA, Canada, Mexico Latin + Digits
China cn China Chinese + Latin + Digits

Pro Tip: Always use specific regions for best accuracy. Only use univ when the region is unknown!

🎯 How to Choose the Right Models

Use Case 1: Parking Management

Requirements: Good accuracy, real-time performance, cost-effective

# Recommended Configuration
detector = ma_anpr_detector_v14(
    "micro_320p_fp32",  # 97% detection, 128 FPS
    user, key, sig,
    backend="cuda",
    conf_thres=0.25
)

ocr = ma_anpr_ocr_v14(
    "small_fp32",       # 95%+ exact match, 300 FPS
    "eup",              # Specific region
    user, key, sig
)

Why: Excellent balance of speed and accuracy. Handles 90%+ of plates easily.

Use Case 2: Security Checkpoint (Critical)

Requirements: Maximum accuracy, can't miss plates

# Recommended Configuration
detector = ma_anpr_detector_v14(
    "large_640p_fp32",  # 99.31% detection (highest!)
    user, key, sig,
    backend="cuda",
    conf_thres=0.20     # Lower threshold for more detections
)

ocr = ma_anpr_ocr_v14(
    "large_fp32",       # 95%+ exact match, best accuracy
    "kr",               # Specific region for your area
    user, key, sig
)

Why: Maximum detection and recognition accuracy. No compromises.

Use Case 3: Traffic Monitoring (High Volume)

Requirements: Maximum speed, process many cameras

# Recommended Configuration
detector = ma_anpr_detector_v14(
    "small_320p_fp32",  # 98% detection, 142 FPS (fastest!)
    user, key, sig,
    backend="cuda",
    conf_thres=0.25
)

ocr = ma_anpr_ocr_v14(
    "small_fp32",       # 300 FPS (fastest OCR!)
    "univ",             # Universal for mixed traffic
    user, key, sig
)

Why: Fastest processing for high-volume applications. Can handle multiple streams.

Use Case 4: Mobile/Edge Device

Requirements: Small size, low power, on-device processing

# Recommended Configuration
detector = ma_anpr_detector_v14(
    "micro_320p_fp16",  # 97% detection, 42 MB (50% smaller!)
    user, key, sig,
    backend="cpu",      # CPU for mobile
    conf_thres=0.25
)

ocr = ma_anpr_ocr_v14(
    "pico_fp32",        # 20 MB, 270 FPS
    "kr",               # Specific region
    user, key, sig
)

Why: Smallest models, excellent for mobile/edge. Total size: 62 MB.

Use Case 5: Law Enforcement (Difficult Conditions)

Requirements: Works in poor lighting, angles, damaged plates

# Recommended Configuration
detector = ma_anpr_detector_v14(
    "medium_640p_fp32", # 99.21% detection
    user, key, sig,
    backend="cuda",
    conf_thres=0.15     # Very low threshold for difficult cases
)

ocr = ma_anpr_ocr_v14(
    "large_fp32",       # Best OCR accuracy
    "na",               # Specific region
    user, key, sig
)

Why: Handles difficult conditions better. Lower threshold catches more plates.

📈 Performance Comparison Chart

Detector Models: Speed vs Accuracy

Category Fastest Balanced Most Accurate
320p small_320p_fp32
142 FPS, 98.00%
micro_320p_fp32
128 FPS, 97.13%
large_320p_fp32
131 FPS, 98.40%
640p small_640p_fp32
70 FPS, 99.15%
medium_640p_fp32
66 FPS, 99.21%
large_640p_fp32
60 FPS, 99.31%
Mobile pico_320p_fp16
50+ FPS, 37 MB
micro_320p_fp16
56 FPS, 42 MB
small_320p_fp16
70+ FPS, 57 MB

OCR Models: Speed vs Accuracy

Priority Smallest Fastest Most Accurate
Choice pico_fp32
20 MB, 270 FPS
91.78% exact
small_fp32
112 MB, 300 FPS
91.54% exact
large_fp32
179 MB, 262 FPS
91.70% exact

💡 Performance Tips

1. GPU Acceleration is Essential

# CPU: ~1-2 FPS (slow!)
detector = ma_anpr_detector_v14(..., backend="cpu")

# CUDA (NVIDIA GPU): ~100+ FPS (fast!)
detector = ma_anpr_detector_v14(..., backend="cuda")

# DirectML (Windows GPU): ~50+ FPS
detector = ma_anpr_detector_v14(..., backend="directml")

Result: GPU is 50-100× faster than CPU!

2. Use Batch Processing

# Slow: Process one by one
for img in images:
    text, conf = ocr.predict(img)

# Fast: Process in batch (3-5× faster!)
results = ocr.predict(images)  # Pass list

3. Choose Resolution Wisely

  • 320p: Good quality images, controlled environment → Use 320p (2× faster)
  • 640p: Poor lighting, far distance, damaged plates → Use 640p (higher accuracy)

4. Tune Confidence Thresholds

# High precision (fewer false positives)
detector = ma_anpr_detector_v14(..., conf_thres=0.50)

# Balanced (recommended)
detector = ma_anpr_detector_v14(..., conf_thres=0.25)

# High recall (catch more plates, more false positives)
detector = ma_anpr_detector_v14(..., conf_thres=0.15)

5. Use Specific Regions

# ❌ Less accurate (universal)
ocr = ma_anpr_ocr_v14("small_fp32", "univ", ...)  # ~92% exact match

# ✅ More accurate (specific region)
ocr = ma_anpr_ocr_v14("small_fp32", "kr", ...)    # ~99% exact match!

🚀 Quick Decision Guide

Your Priority Detector OCR
Best Overall micro_320p_fp32 small_fp32
Fastest small_320p_fp32 small_fp32
Most Accurate large_640p_fp32 large_fp32
Smallest pico_320p_fp16 pico_fp32
Mobile micro_320p_fp16 pico_fp32
Balanced medium_320p_fp32 medium_fp32

📊 Benchmark Environment

  • GPU: NVIDIA RTX 3060 (CUDA 11.8)
  • CPU: Intel Core i7
  • Dataset: Real-world license plate images
  • Test Size: 1000+ images per region
  • Updated: December 2025

🎓 Key Takeaways

  • Two-stage pipeline: Detector → OCR
  • Mix and match models for your needs
  • 320p models: 2× faster, excellent for most uses
  • 640p models: Highest accuracy for difficult cases
  • GPU acceleration: 50-100× faster than CPU
  • Specific regions: Much better accuracy than universal
  • Batch processing: 3-5× faster for multiple images
  • Best overall: micro_320p_fp32 + small_fp32

💻 Example Configuration

from marearts_anpr import ma_anpr_detector_v14, ma_anpr_ocr_v14
from marearts_anpr import marearts_anpr_from_image_file

# Initialize models (one time)
detector = ma_anpr_detector_v14(
    "micro_320p_fp32",      # 97% detection, 128 FPS
    user_name, serial_key, signature,
    backend="cuda",          # GPU acceleration
    conf_thres=0.25          # Balanced threshold
)

ocr = ma_anpr_ocr_v14(
    "small_fp32",            # 95%+ accuracy, 300 FPS
    "eup",                   # Specific region for best accuracy
    user_name, serial_key, signature
)

# Process image
result = marearts_anpr_from_image_file(detector, ocr, "plate.jpg")
print(result)

# Output:
# {
#   "results": [
#     {
#       "ocr": "AB-123-CD",
#       "ocr_conf": 98.5,
#       "ltrb": [120, 230, 380, 290],
#       "ltrb_conf": 95
#     }
#   ],
#   "ltrb_proc_sec": 0.008,  # Detection time
#   "ocr_proc_sec": 0.003     # OCR time
# }

🔗 Resources

  • 📊 Full Benchmarks: See detailed results in GitHub docs
  • 📚 Model Guide: Complete model documentation
  • 🧪 Try Free: ma-anpr test-api image.jpg
  • 🛒 Get License: MareArts ANPR

🎯 Conclusion

MareArts ANPR V14 offers 11 detector models and 5 OCR models, giving you 55+ possible combinations! The right choice depends on your specific requirements:

  • Speed-critical? → small_320p_fp32 + small_fp32
  • Accuracy-critical? → large_640p_fp32 + large_fp32
  • Balanced? → micro_320p_fp32 + small_fp32 (recommended!)
  • Mobile? → micro_320p_fp16 + pico_fp32

Start with the recommended configuration and tune based on your results. Happy optimizing! ⚡🚗


Labels: ANPR, MachineLearning, ComputerVision, Performance, Benchmarks, Models, Metrics, DeepLearning, Optimization, GPU

MareArts ANPR V14 - Advanced Manual Processing & Performance Tuning

 

⚡ MareArts ANPR V14 - Advanced Manual Processing

Ready to take control? In this advanced guide, I'll show you how to manually process detections, measure performance, and optimize for your specific use case.

🎯 Why Manual Processing?

  • Full control over detection pipeline
  • Custom filtering and post-processing
  • Performance measurement and optimization
  • Integration with existing computer vision pipelines
  • Custom confidence thresholds per stage

🔧 Manual Detection & OCR Pipeline

from marearts_anpr import ma_anpr_detector_v14, ma_anpr_ocr_v14
import cv2
from PIL import Image
import time

# Initialize models
detector = ma_anpr_detector_v14(
    "medium_640p_fp32",
    user_name, serial_key, signature,
    backend="cpu",
    conf_thres=0.25,
    iou_thres=0.5
)

ocr = ma_anpr_ocr_v14("medium_fp32", "eup", user_name, serial_key, signature)

# Load image
img = cv2.imread("plate.jpg")

# Step 1: Detect license plates
start = time.time()
detections = detector.detector(img)
detection_time = time.time() - start

print(f"Detection time: {detection_time:.4f}s")
print(f"Found {len(detections)} plate(s)")

# Step 2: Process each detection
results = []
ocr_time = 0

for i, box_info in enumerate(detections):
    # Get bounding box
    bbox = box_info['bbox']  # [x1, y1, x2, y2]
    score = box_info['score']  # Detection confidence
    
    # Crop plate region
    x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])
    crop = img[y1:y2, x1:x2]
    
    if crop.size == 0:
        continue
    
    # Convert to PIL for OCR
    pil_img = Image.fromarray(crop)
    if pil_img.mode != "RGB":
        pil_img = pil_img.convert("RGB")
    
    # Run OCR
    start = time.time()
    text, confidence = ocr.predict(pil_img)
    elapsed = time.time() - start
    ocr_time += elapsed
    
    print(f"Plate {i+1}: {text} ({confidence}%) - {elapsed:.4f}s")
    
    results.append({
        "ocr": text,
        "ocr_conf": confidence,
        "bbox": [x1, y1, x2, y2],
        "det_conf": int(score * 100)
    })

print(f"\nTotal time: {detection_time + ocr_time:.4f}s")

📊 Detection Object Structure

# detector.detector(img) returns list of dictionaries:
[
    {
        'bbox': [x1, y1, x2, y2],  # Bounding box coordinates
        'score': 0.95,              # Detection confidence (0-1)
        'class': 'license_plate'    # Object class
    },
    ...
]

# ocr.predict(pil_image) returns tuple:
("ABC1234", 98.5)  # (text, confidence_percentage)

🚀 Backend Performance Comparison

backends = ["cpu", "cuda"]  # Add "directml" on Windows

for backend_name in backends:
    try:
        print(f"\n🔧 Testing {backend_name}...")
        
        # Initialize with specific backend
        test_detector = ma_anpr_detector_v14(
            "medium_640p_fp32",
            user_name, serial_key, signature,
            backend=backend_name,
            conf_thres=0.25
        )
        
        # Measure performance
        start = time.time()
        detections = test_detector.detector(img)
        elapsed = time.time() - start
        
        print(f"Detected {len(detections)} plates in {elapsed:.4f}s")
        print(f"Speed: {1/elapsed:.1f} FPS")
        
    except Exception as e:
        print(f"⚠️ {backend_name} not available: {e}")

⚙️ Performance Results (Typical)

Backend Detection OCR Total FPS
CPU (i7) ~0.15s ~0.03s ~0.18s ~5.5
CUDA (RTX 3060) ~0.008s ~0.002s ~0.01s ~100

Result: GPU acceleration = 18x faster! 🚀

🎛️ Custom Filtering

# Filter detections by confidence
min_detection_conf = 0.50
min_ocr_conf = 80.0

filtered_results = []

for box_info in detections:
    if box_info['score'] < min_detection_conf:
        continue  # Skip low confidence detections
    
    # Process with OCR...
    text, conf = ocr.predict(plate_crop)
    
    if conf < min_ocr_conf:
        continue  # Skip low confidence OCR
    
    filtered_results.append({
        "text": text,
        "confidence": conf,
        "bbox": bbox
    })

print(f"After filtering: {len(filtered_results)} high-confidence plates")

🎨 Custom Visualization

import cv2

# Draw boxes and text on image
for result in results:
    x1, y1, x2, y2 = result['bbox']
    text = result['ocr']
    conf = result['ocr_conf']
    
    # Draw rectangle
    cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
    
    # Draw text
    label = f"{text} ({conf}%)"
    cv2.putText(img, label, (x1, y1-10), 
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

cv2.imwrite("result.jpg", img)

📹 Video Processing Pipeline

import cv2

# Open video
cap = cv2.VideoCapture("traffic.mp4")

frame_count = 0
plate_history = {}

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break
    
    frame_count += 1
    
    # Process every N frames (skip frames for speed)
    if frame_count % 5 != 0:
        continue
    
    # Detect plates
    detections = detector.detector(frame)
    
    for det in detections:
        bbox = det['bbox']
        x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])
        crop = frame[y1:y2, x1:x2]
        
        if crop.size == 0:
            continue
        
        # OCR
        pil_crop = Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
        text, conf = ocr.predict(pil_crop)
        
        # Track plates (simple tracking by position)
        plate_id = f"{x1//50}_{y1//50}"
        
        if plate_id not in plate_history:
            plate_history[plate_id] = []
        plate_history[plate_id].append(text)
        
        # Draw
        cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
        cv2.putText(frame, text, (x1, y1-10), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    
    cv2.imshow('ANPR', frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

# Print detected plates
print("\nDetected plates:")
for plate_id, texts in plate_history.items():
    # Most common text for this plate
    most_common = max(set(texts), key=texts.count)
    print(f"  {most_common} (seen {len(texts)} times)")

💾 Batch Processing from Directory

import os
from pathlib import Path

image_dir = Path("./images")
results_all = {}

for img_path in image_dir.glob("*.jpg"):
    print(f"Processing {img_path.name}...")
    
    img = cv2.imread(str(img_path))
    detections = detector.detector(img)
    
    plates = []
    for det in detections:
        bbox = det['bbox']
        x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])
        crop = img[y1:y2, x1:x2]
        
        if crop.size > 0:
            pil_crop = Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
            text, conf = ocr.predict(pil_crop)
            plates.append({"text": text, "conf": conf})
    
    results_all[img_path.name] = plates

# Save results
import json
with open("results.json", "w") as f:
    json.dump(results_all, f, indent=2)

print(f"\nProcessed {len(results_all)} images")

🎓 Advanced Tips

  • GPU Memory: Use cuda backend for 10-100x speedup
  • Confidence Tuning: Lower conf_thres to 0.15-0.20 for difficult images
  • IOU Threshold: Increase iou_thres to reduce duplicate detections
  • Batch Processing: Process multiple crops at once with ocr.predict([img1, img2, ...])
  • Frame Skipping: Process every Nth frame in videos for speed
  • Multi-threading: Run detector and OCR in separate threads

🔍 Troubleshooting

No detections?

  • Lower conf_thres to 0.15
  • Try larger model (large_640p_fp32)
  • Check image quality and resolution

Wrong OCR results?

  • Verify correct region (kr, eup, na, cn)
  • Try larger OCR model (large_fp32)
  • Check plate crop quality

Slow performance?

  • Use GPU backend (cuda or directml)
  • Use smaller models (small_640p_fp32, small_fp32)
  • Skip video frames
  • Batch process multiple images

💡 Conclusion

Manual processing gives you complete control over the ANPR pipeline. Use it for:

  • ✅ Custom filtering and validation
  • ✅ Performance optimization
  • ✅ Video stream processing
  • ✅ Integration with existing CV pipelines
  • ✅ Advanced visualization and tracking

Happy optimizing! ⚡🚗



8/25/2025

GPU memory vs shared memory



CK Tile Tutorial Day 2 (AMD hip programming) - Simple GEMM.

 Concepts Added:

  • 2D grid/block configuration
  • Matrix multiplication basics
  • Each thread computes one output element

Key Pattern:

// Each thread computes C[row][col]
for (int k = 0; k < K; k++) {
    sum += A[row][k] * B[k][col];
}
.
=== Thread Mapping Visualization ===
Each thread computes one C[i][j]:

  Block(0,0)        Block(1,0)
  ┌─────────┐      ┌─────────┐
  │T00 T01..│      │T00 T01..│
  │T10 T11..│      │T10 T11..│
  │... ... ..│      │... ... ..│
  └─────────┘      └─────────┘
       ↓                ↓
  C[0:16,0:16]    C[0:16,16:32]

Each thread's work:
  for k in 0..K:
    sum += A[row][k] * B[k][col]
  C[row][col] = sum

=== Step 2: Simple GEMM ===
Matrix multiply: (64x64) * (64x64) = (64x64)
Launching with grid(4,4), block(16,16)
Result: CORRECT
Time: 0.4232 ms
Performance: 1.23887 GFLOPS

=== Step 2: Simple GEMM ===
Matrix multiply: (128x128) * (128x128) = (128x128)
Launching with grid(8,8), block(16,16)
Result: CORRECT
Time: 0.03824 ms
Performance: 109.684 GFLOPS

Key Concepts Added:
1. 2D grid/block configuration
2. Each thread computes one output element
3. Row-major vs column-major layouts
4. Performance measurement (GFLOPS)
..

code
.
// Step 2: Simple GEMM (Matrix Multiplication)
// Building on Step 1, now each thread computes one output element

#include <hip/hip_runtime.h>
#include <iostream>
#include <vector>

// ============================================
// PART 1: Kernel Arguments
// ============================================
struct SimpleGemmKernelArgs {
const float* a_ptr; // M x K matrix
const float* b_ptr; // K x N matrix
float* c_ptr; // M x N matrix
int M;
int N;
int K;
SimpleGemmKernelArgs(const float* a, const float* b, float* c,
int m, int n, int k)
: a_ptr(a), b_ptr(b), c_ptr(c), M(m), N(n), K(k) {}
};

// ============================================
// PART 2: The Kernel (One thread per output)
// ============================================
struct SimpleGemmKernel {
static dim3 GridSize(const SimpleGemmKernelArgs& args) {
// 16x16 threads per block
int grid_m = (args.M + 15) / 16;
int grid_n = (args.N + 15) / 16;
return dim3(grid_n, grid_m, 1); // Note: x=N, y=M
}
static dim3 BlockSize() {
return dim3(16, 16, 1); // 16x16 = 256 threads
}
__device__ void operator()(const SimpleGemmKernelArgs& args) const {
// Each thread computes one element of C
int col = blockIdx.x * blockDim.x + threadIdx.x; // N dimension
int row = blockIdx.y * blockDim.y + threadIdx.y; // M dimension
// Bounds check
if (row >= args.M || col >= args.N) return;
// Compute dot product for C[row][col]
float sum = 0.0f;
for (int k = 0; k < args.K; k++) {
// A is row-major: A[row][k] = A[row * K + k]
// B is column-major: B[k][col] = B[k + col * K]
float a_val = args.a_ptr[row * args.K + k];
float b_val = args.b_ptr[k + col * args.K];
sum += a_val * b_val;
}
// Store result (C is row-major)
args.c_ptr[row * args.N + col] = sum;
}
};

// ============================================
// PART 3: Host Code
// ============================================
__global__ void simple_gemm_kernel(SimpleGemmKernelArgs args) {
SimpleGemmKernel kernel;
kernel(args);
}

void run_simple_gemm(int M, int N, int K) {
std::cout << "\n=== Step 2: Simple GEMM ===\n";
std::cout << "Matrix multiply: (" << M << "x" << K << ") * ("
<< K << "x" << N << ") = (" << M << "x" << N << ")\n";
// Allocate host memory
std::vector<float> h_a(M * K);
std::vector<float> h_b(K * N);
std::vector<float> h_c(M * N, 0.0f);
// Initialize with simple values
for (int i = 0; i < M * K; i++) h_a[i] = 1.0f;
for (int i = 0; i < K * N; i++) h_b[i] = 2.0f;
// Allocate device memory
float *d_a, *d_b, *d_c;
hipMalloc(&d_a, M * K * sizeof(float));
hipMalloc(&d_b, K * N * sizeof(float));
hipMalloc(&d_c, M * N * sizeof(float));
// Copy to device
hipMemcpy(d_a, h_a.data(), M * K * sizeof(float), hipMemcpyHostToDevice);
hipMemcpy(d_b, h_b.data(), K * N * sizeof(float), hipMemcpyHostToDevice);
// Create kernel arguments
SimpleGemmKernelArgs args(d_a, d_b, d_c, M, N, K);
// Get launch configuration
dim3 grid = SimpleGemmKernel::GridSize(args);
dim3 block = SimpleGemmKernel::BlockSize();
std::cout << "Launching with grid(" << grid.x << "," << grid.y
<< "), block(" << block.x << "," << block.y << ")\n";
// Launch kernel
hipEvent_t start, stop;
hipEventCreate(&start);
hipEventCreate(&stop);
hipEventRecord(start);
simple_gemm_kernel<<<grid, block>>>(args);
hipEventRecord(stop);
hipEventSynchronize(stop);
float milliseconds = 0;
hipEventElapsedTime(&milliseconds, start, stop);
// Copy result back
hipMemcpy(h_c.data(), d_c, M * N * sizeof(float), hipMemcpyDeviceToHost);
// Verify (each element should be K * 1.0 * 2.0 = 2K)
float expected = 2.0f * K;
bool correct = true;
for (int i = 0; i < std::min(10, M*N); i++) {
if (h_c[i] != expected) {
correct = false;
break;
}
}
std::cout << "Result: " << (correct ? "CORRECT" : "WRONG") << "\n";
std::cout << "Time: " << milliseconds << " ms\n";
// Calculate FLOPS
double flops = 2.0 * M * N * K; // 2 ops per multiply-add
double gflops = (flops / milliseconds) / 1e6;
std::cout << "Performance: " << gflops << " GFLOPS\n";
// Cleanup
hipFree(d_a);
hipFree(d_b);
hipFree(d_c);
hipEventDestroy(start);
hipEventDestroy(stop);
}

// ============================================
// VISUALIZATION: How threads map to output
// ============================================
void visualize_thread_mapping() {
std::cout << "\n=== Thread Mapping Visualization ===\n";
std::cout << "Each thread computes one C[i][j]:\n\n";
std::cout << " Block(0,0) Block(1,0)\n";
std::cout << " ┌─────────┐ ┌─────────┐\n";
std::cout << " │T00 T01..│ │T00 T01..│\n";
std::cout << " │T10 T11..│ │T10 T11..│\n";
std::cout << " │... ... ..│ │... ... ..│\n";
std::cout << " └─────────┘ └─────────┘\n";
std::cout << " ↓ ↓\n";
std::cout << " C[0:16,0:16] C[0:16,16:32]\n\n";
std::cout << "Each thread's work:\n";
std::cout << " for k in 0..K:\n";
std::cout << " sum += A[row][k] * B[k][col]\n";
std::cout << " C[row][col] = sum\n";
}

// ============================================
// PART 4: Main
// ============================================
int main() {
std::cout << "MareArts CK Tile Tutorial - Step 2: Simple GEMM\n";
std::cout << "======================================\n";
visualize_thread_mapping();
// Run with different sizes
run_simple_gemm(64, 64, 64);
run_simple_gemm(128, 128, 128);
std::cout << "\nKey Concepts Added:\n";
std::cout << "1. 2D grid/block configuration\n";
std::cout << "2. Each thread computes one output element\n";
std::cout << "3. Row-major vs column-major layouts\n";
std::cout << "4. Performance measurement (GFLOPS)\n";
std::cout << "\nProblem: Each thread reads K elements from A and B\n";
std::cout << " → Poor memory reuse!\n";
std::cout << "Next: Add tiling and shared memory for efficiency\n";
return 0;
}
..

🙇🏻‍♂️
MareArts

8/24/2025

CK Tile Tutorial Day 1 (AMD hip programming) - Vector add.

.

Concepts:

  • Basic kernel structure: Args → Kernel → operator()
  • Grid/Block configuration
  • One thread per element processing

Key Code:

struct VectorAddKernel {
    __device__ void operator()(args) {
        int idx = blockIdx.x * blockDim.x + threadIdx.x;
        c[idx] = a[idx] + b[idx];
    }
};


..

code

..

// Step 1: Simplest CK Tile Kernel - Vector Addition
// This demonstrates the absolute basics of CK Tile

#include <hip/hip_runtime.h>
#include <iostream>
#include <vector>

// ============================================
// PART 1: Kernel Arguments (Host → Device)
// ============================================
struct VectorAddKernelArgs {
const float* a_ptr;
const float* b_ptr;
float* c_ptr;
int n;
// Constructor from host arguments
VectorAddKernelArgs(const float* a, const float* b, float* c, int size)
: a_ptr(a), b_ptr(b), c_ptr(c), n(size) {}
};

// ============================================
// PART 2: The Kernel
// ============================================
struct VectorAddKernel {
// Static method to get grid size (how many blocks)
static dim3 GridSize(const VectorAddKernelArgs& args) {
// 256 threads per block, divide work
int blocks = (args.n + 255) / 256;
return dim3(blocks, 1, 1);
}
// Static method to get block size (threads per block)
static dim3 BlockSize() {
return dim3(256, 1, 1);
}
// The actual kernel function - called by each thread
__device__ void operator()(const VectorAddKernelArgs& args) const {
// Calculate global thread index
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// Check bounds
if (idx < args.n) {
// Each thread does one element
args.c_ptr[idx] = args.a_ptr[idx] + args.b_ptr[idx];
}
}
};

// ============================================
// PART 3: Host Launch Function
// ============================================
__global__ void vector_add_kernel(VectorAddKernelArgs args) {
VectorAddKernel kernel;
kernel(args);
}

void run_vector_add(int n) {
std::cout << "\n=== Step 1: Vector Addition ===\n";
std::cout << "Adding two vectors of size " << n << "\n";
// Allocate host memory
std::vector<float> h_a(n, 1.0f);
std::vector<float> h_b(n, 2.0f);
std::vector<float> h_c(n, 0.0f);
// Allocate device memory
float *d_a, *d_b, *d_c;
hipMalloc(&d_a, n * sizeof(float));
hipMalloc(&d_b, n * sizeof(float));
hipMalloc(&d_c, n * sizeof(float));
// Copy to device
hipMemcpy(d_a, h_a.data(), n * sizeof(float), hipMemcpyHostToDevice);
hipMemcpy(d_b, h_b.data(), n * sizeof(float), hipMemcpyHostToDevice);
// Create kernel arguments
VectorAddKernelArgs args(d_a, d_b, d_c, n);
// Get launch configuration
dim3 grid = VectorAddKernel::GridSize(args);
dim3 block = VectorAddKernel::BlockSize();
std::cout << "Launching with grid(" << grid.x << "), block(" << block.x << ")\n";
// Launch kernel
vector_add_kernel<<<grid, block>>>(args);
// Copy result back
hipMemcpy(h_c.data(), d_c, n * sizeof(float), hipMemcpyDeviceToHost);
// Verify
bool correct = true;
for (int i = 0; i < std::min(10, n); i++) {
if (h_c[i] != 3.0f) {
correct = false;
break;
}
}
std::cout << "Result: " << (correct ? "CORRECT" : "WRONG") << "\n";
std::cout << "First 5 elements: ";
for (int i = 0; i < std::min(5, n); i++) {
std::cout << h_c[i] << " ";
}
std::cout << "\n";
// Cleanup
hipFree(d_a);
hipFree(d_b);
hipFree(d_c);
}

// ============================================
// PART 4: Main
// ============================================
int main() {
std::cout << "MareArts CK Tile Tutorial - Step 1: Vector Addition\n";
std::cout << "==========================================\n";
// Run with different sizes
run_vector_add(1024);
run_vector_add(10000);
std::cout << "\nKey Concepts Demonstrated:\n";
std::cout << "1. Kernel structure: Args → Kernel → operator()\n";
std::cout << "2. Grid/Block configuration\n";
std::cout << "3. Each thread processes one element\n";
std::cout << "4. Bounds checking for safety\n";
return 0;
}

...


Result

CK Tile Tutorial - Step 1: Vector Addition

==========================================


=== Step 1: Vector Addition ===

Adding two vectors of size 1024

Launching with grid(4), block(256)

Result: CORRECT

First 5 elements: 3 3 3 3 3 


=== Step 1: Vector Addition ===

Adding two vectors of size 10000

Launching with grid(40), block(256)

Result: CORRECT

First 5 elements: 3 3 3 3 3 


Key Concepts Demonstrated:

1. Kernel structure: Args → Kernel → operator()

2. Grid/Block configuration

3. Each thread processes one element

4. Bounds checking for safety