def sensor_id(sensor: Sensor | int | None) -> int has three bars in one signature and
looks like bit arithmetic. It is not arithmetic and it is not new syntax to memorise: each bar is the
word or, and the whole annotation is one sentence — "a Sensor, or an
int, or nothing".
MareArts ANPR is a license-plate recognition app for iOS and Android,
with a web viewer and a desktop viewer on the same account.
Plates are read on the phone. Cloud MMC (make, model, colour, type, side, nation),
team sync, rules, maps, and stats sit on
marearts.com.
Two acronyms one letter apart, describing promises made at two different moments. An API is what
you agree to when you write the call. An ABI is what your compiled bytes agree to when they meet
somebody else's compiled bytes — and unlike the API, breaking it usually produces no error at all.
C forgets the name of every field the moment it finishes compiling. The X-macro is how a C
programmer gets those names back — by writing the list once and replaying it as an enum, a string
table, a descriptor table, and a set of registration calls that run before main.
def append_to(item, target=[]) looks like it starts from an empty list every time. It does
not. One list is created when the def statement runs, and every call for the rest of the
program's life shares it.
You define a function inside a loop, collect the functions, call them later — and every single one
returns the last value. The usual fix is a strange-looking line where the same name appears twice.
Here is what is actually going on, and why the fix works.
Most pytest tutorials stop right after assert 1 + 1 == 2, which is roughly where the
interesting part begins. This one keeps going — through fixtures, parametrization and the
configuration that makes a suite pleasant to live with, and out the other side into hooks and
plugins.
When you read Python code, sooner or later you hit a line like this:
data: Dict[str, int] = {}
The : Dict[str, int] part looks strange the first time. In this post we'll
figure out exactly what that one line means — and whether a "type hint" is actually
enforced — using tiny runnable examples.
1. One line, three things at once
That single line is really three pieces glued together.
data : Dict[str, int] = {}
│ │ │
name type hint actual
(a note) value
Piece
Meaning
data
the variable name
: Dict[str, int]
a type hint — "keys are str, values are int". Just a note for humans/editors
= {}
the actual value = an empty dict
At runtime it is exactly the same as:
data = {} # drop the hint and this is all that's left
2. = {} is just a dict
A dict is a "key → value" store. It has nothing to do with type hints — it's a basic
built-in type that has always existed.
data = {} # empty dict
data["gemm"] = "A" # key (str) -> value
data["conv2d"] = "B"
print(data) # {'gemm': 'A', 'conv2d': 'B'}
print(data["gemm"]) # A
print(list(data)) # ['gemm', 'conv2d']
3. : Dict[str, int] is a "type hint"
Dict[str, int] is a note saying "a dict whose keys are strings and values
are integers". Python does not enforce that note when it runs.
from typing import Dict
ages: Dict[str, int] = {} # at runtime this is just ages = {}
ages["alice"] = 30
ages["bob"] = 25
print(ages) # {'alice': 30, 'bob': 25}
4. Is it really not enforced? — break it on purpose
Violating the hint does not raise an error. Let's prove it.
from typing import Dict
ages: Dict[str, int] = {} # promise: "str keys, int values"
ages["alice"] = 30 # keeps the promise
ages["oops"] = "not-int" # value is a string -> breaks it
ages[123] = 99 # key is an int -> breaks it
print(ages)
Output:
{'alice': 30, 'oops': 'not-int', 123: 99}
Key point: a type hint is not enforced. The Python
interpreter simply ignores it at runtime, so breaking it still runs fine.
5. So why write hints at all?
If they're not enforced, why bother? Because they're a "quality tool", not a rule.
Humans — reading the code, you instantly see "this is a str→int dict".
Editors (IDEs) — autocomplete, and a red squiggle when you misuse it.
Static checkers (mypy) — run mypy file.py separately and it catches
violations before you run the code.
A hint is "a promise + documentation", not a check. To actually catch violations,
run a tool like mypy yourself — the interpreter won't.
6. Dict vs dict — and when did this syntax appear?
The dict itself is old; the type-hint syntax is the newer part.
Syntax
Import needed?
Introduced
Dict[str, int] (capital)
from typing import Dict
Python 3.5 (2015)
dict[str, int] (lowercase)
none (built-in)
Python 3.9 (2020)
dict (plain)
none
always
Annotating a variable with x: int = 0 has been possible since Python 3.6 (2016).
Both forms mean the same thing, so new code usually prefers the lowercase dict[...].
A GPU kernel launch is a hierarchy of work units. Bigger units contain smaller ones.
AMD term
NVIDIA term
What it is
Grid
Grid
The whole kernel launch — covers the entire problem.
Workgroup
Block / threadblock
A group of threads on one Compute Unit (CU). Shares LDS (shared memory). Can synchronize via __syncthreads().
Wavefront (wave)
Warp
64 threads (AMD CDNA) executing the same instruction simultaneously (SIMT).
Thread (work-item)
Thread
One 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.
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
Type
Size
Scope
Notes
VGPR (vector GPR)
32-bit (4 B)
Private per lane
Up to 256 per lane per wave. Each lane sees its own VGPR.
SGPR (scalar GPR)
32-bit (4 B)
Shared by 64 lanes
Used for scalar values like loop counters, addresses.
AGPR (accumulator GPR)
32-bit (4 B)
Private per lane
CDNA-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.
GPU memory has multiple levels, similar to CPU cache hierarchy:
Level
Size (per CU / total)
Latency (cycles)
Managed by
CPU analogue
Registers (VGPR/SGPR)
~256 KB / CU
~1
Compiler
CPU registers
LDS (shared memory)
64 KB / CU
~10–30
Software (explicit loads/stores)
Scratchpad / fast SRAM
L1 cache
16 KB / CU
~30
Hardware (transparent)
L1 cache
L2 cache
~16 MB total
~150
Hardware
L2 cache
Infinity / L3
~256 MB
~300
Hardware
L3 cache
HBM (global memory)
192 GB
~500–1000
HW + software
DRAM
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);
Parameter
Meaning
Example
block (a.k.a. blockSize)
Threads per workgroup
256 → 4 waves per workgroup
grid
Number of workgroups
4 → 4 workgroups total
(implicit)
Wave count = blockSize / 64
256 / 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 type
Size (B)
vec=1
vec=2
vec=4
vec=8
vec=16
fp16 / bf16
2
2 B
4 B
8 B
16 B (max)
—
fp32
4
4 B
8 B
16 B (max)
—
—
int8
1
1 B
2 B
4 B
8 B
16 B (max)
Rule:vec × sizeof(dtype) ≤ 16 byte. Larger vec means fewer load instructions to move the same amount of data — faster.
__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).
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.
Pattern
Who covers one tile
Wave layout inside tile
thread_raked
1 wave (64 threads)
n/a (single wave)
warp_raked
Multiple waves cooperate
1D stack (waves stripe along one axis)
block_raked
All waves of the workgroup
2D 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)