The DDR bandwidth is significantly lower than the claimed 273GB/s

Why did I test the DDR bandwidth using lmbench and find that the maximum bandwidth was only 135GB/s, which was much lower than the claimed 273GB/s? What’s going on here?

Why does the total width of DDR read by using dmidcode -t memory only show 32 bits?

dmidecode 3.5

Getting SMBIOS data from sysfs.
SMBIOS 3.3.0 present.

Handle 0x0010, DMI type 16, 23 bytes
Physical Memory Array
Location: System Board Or Motherboard
Use: System Memory
Error Correction Type: None
Maximum Capacity: 128 GB
Error Information Handle: No Error
Number Of Devices: 1

Handle 0x0011, DMI type 17, 92 bytes
Memory Device
Array Handle: 0x0010
Error Information Handle: Not Provided
Total Width: 32 bits
Data Width: 32 bits
Size: 128 GB
Form Factor: Chip
Set: None
Locator: DIMM0
Bank Locator: BANK 0
Type: LPDDR5
Type Detail: Synchronous
Speed: 8533 MT/s
Manufacturer: SK Hynix
Serial Number: To Be Filled by OEM
Asset Tag: To Be Filled by OEM
Part Number: To Be Filled by OEM
Rank: Unknown
Configured Memory Speed: 8533 MT/s
Minimum Voltage: Unknown
Maximum Voltage: Unknown
Configured Voltage: Unknown
Memory Technology: DRAM
Memory Operating Mode Capability: Volatile memory
Firmware Version: Not Specified
Module Manufacturer ID: Bank 7, Hex 0x00
Module Product ID: Unknown
Memory Subsystem Controller Manufacturer ID: Unknown
Memory Subsystem Controller Product ID: Unknown
Non-Volatile Size: None
Volatile Size: 128 GB
Cache Size: None
Logical Size: None

Are there any other methods to monitor the DDR bandwidth during GPU inference?

Here is my summary of last few days as to the mem bandwidth.

Those are not laboratory tests and I’m not an expert in this area. But it gave me ideas on what I’m working with.

Hardware: NVIDIA GB10, 48 SMs, SM 12.1, 119.63 GiB LPDDR5X unified memory pool
Theoretical peak: 273 GB/s (LPDDR5X, 8533 MT/s)
Nodes tested: Spark 8 (single-node), Spark 5-8 (4-node cluster)


1. UMA Bandwidth Diagnostics — Idle Baseline (March 10)

Method

Custom CUDA bandwidth tests (bw_small.cu, 8MB buffers) and uma_safe_test (128MB buffers) on idle Spark nodes.

Results

Test Copy (GB/s) Triad (GB/s) Notes
128MB idle baseline 229 230 Near theoretical peak
8MB idle baseline 191 97 L2 cache effects at small working set
Prefetch bandwidth 225-273 — No-op when data already resident
Coherence test PASS — UMA coherence correct

Key finding: GB10 achieves 84% of theoretical peak (229/273 GB/s) with large buffers on idle nodes. The UMA architecture is healthy — no hardware defects or misconfiguration.


2. Bandwidth Contention Under Inference (March 11)

Method

Ran bw_small.cu (8MB Copy/Triad, 50 reps) concurrently with active vLLM inference on Spark 8 (Qwen3.5-35B-A3B, TP=1, bf16).

Three-State Comparison

State Copy GB/s Triad GB/s Drop vs Idle
Fully idle (no model) 190.9 96.8 baseline
Model loaded, GPU idle 187.0 82.1 -2% / -15%
Active decode (GPU 96%) 86.8 39.1 -55% / -60%

Key Observations

  1. Static memory footprint has minimal impact: 65.53 GiB of model weights in memory causes only 2% Copy bandwidth reduction. The -15% Triad drop is TLB/cache metadata pressure, not bandwidth consumption.

  2. Active inference dominates bandwidth: During decode, available bandwidth drops 55-60%. The inference kernels consume the majority of LPDDR5X bandwidth.

  3. Bursty access pattern: Bandwidth during inference ranged widely (Copy: 80-162 GB/s). MoE expert routing creates high-pressure bursts followed by brief recovery windows.

  4. Contention floor: ~80-90 GB/s Copy appears to be the floor for any active decode workload on GB10, regardless of model size. This represents gaps between GEMM kernel memory access bursts.

Cross-Model Comparison

Model Active Params Memory/Node Copy BW During Decode
Qwen3.5-35B-A3B (TP=1) 3B 65.53 GiB ~87 GB/s
Qwen3.5-397B-A17B (TP=8) 17B ~93 GiB ~93 GB/s

Despite 5.6x fewer active parameters, the 35B model creates nearly identical bandwidth contention. Bandwidth contention is driven by kernel access patterns, not model size.


3. Kernel Profiling — Option A vs Option B (March 11)

The Question

Given ~55% bandwidth drop during inference, two explanations were possible:

  • Option A: Kernels efficient, using ~140-178 GB/s for useful work → hardware-limited
  • Option B: Kernels inefficient, wasting bandwidth on misalignment, TLB misses, spills

Profiling Method

Tool: PyTorch Profiler via vLLM’s built-in --profiler-config

  • nsys was attempted first but cannot trace into vLLM’s EngineCore subprocess (spawned via Python multiprocessing.spawn, invisible to --trace-fork-before-exec)
  • --enforce-eager used to disable CUDAGraphs for per-kernel visibility
  • 10 decode iterations profiled (delay_iterations=2 to skip warmup)

Kernel Breakdown (10 decode steps, 337.3ms total CUDA time)

Category CUDA Time % Total
GEMM (cutlass WMMA bf16) 188.6ms 55.9%
MoE (fused expert kernel) 94.4ms 28.0%
GDN Attention 17.1ms 5.1%
Overhead (sort, norm, memcpy, etc.) 37.2ms 11.0%

GEMM + MoE = 83.9% of all CUDA time — dominated by NVIDIA’s own highly-optimized cutlass kernels.

Top Kernels

Rank Kernel Time % Calls
1 cutlass WMMA tensorop bf16 GEMM 141.7ms 42.0% 2000
2 fused_moe_kernel 94.4ms 28.0% 800
3 cutlass WMMA tensorop bf16 GEMM (var) 46.9ms 13.9% 310
4 fused_recurrent_gated_delta_rule_fwd 17.1ms 5.1% 300
5 FlashAttention (flash_fwd_splitkv) 0.7ms 0.2% 100

Bandwidth Utilization

Active parameters per token:    3B (MoE routes to expert subset)
Weight data per decode step:    3B × 2 bytes (bf16) = 6 GB
Decode step time:               33.7ms
Weight-read bandwidth:          6 GB / 0.0337s = 178 GB/s  (65% of 273 GB/s peak)
Total traffic estimate (1.5x):  ~267 GB/s                   (97% of 273 GB/s peak)

Verdict: Option A — Hardware-Limited

The inference is bandwidth-bound at the hardware limit. Evidence:

  1. 84% of CUDA time in optimized NVIDIA cutlass + fused MoE kernels — state-of-the-art for bf16 GEMM
  2. Kernel overhead minimal (11%) — no scheduling gaps, no excessive small kernel launches
  3. FlashAttention negligible (0.2%) — attention is NOT the bottleneck
  4. Per-step timing matches throughput exactly — 33.7ms = 29.7 tok/s (observed: 30.3 tok/s)
  5. Bandwidth utilization ~65-97% of theoretical peak

4. Compute Bandwidth Under Load — 4-Node Cluster (March 10)

Measured on Spark 5-8 during active Qwen3.5-397B-A17B inference (TP=4):

Metric Value
Compute BW under load 82-105 GB/s per node
Prefetch BW (data resident) 225-273 GB/s
Coherence PASS
Aggregate cluster BW consumed ~340-420 GB/s (4 nodes)

The 82-105 GB/s residual bandwidth per node during 4-node inference is consistent with the single-node measurement (~87 GB/s), confirming the contention floor is per-node, not aggregate.


5. Performance Conclusions

Current tok/s Performance

Configuration tok/s Bottleneck
35B-A3B, TP=1, bf16, single node 30.3 Memory bandwidth (178 GB/s weight reads)
397B-A17B, TP=8, INT4, 4 nodes 37.3 Memory bandwidth + NCCL overhead

Fundamental Limits

  • ~80-90 GB/s Copy is the contention floor for any co-located workload during active decode
  • Bandwidth contention is structural — driven by kernel access patterns, not model size
  • No software optimization can eliminate this — the memory controller is saturated by design
  • Quantization is the highest-impact optimization — directly reduces the dominant cost (weight reads)

Test Tools & Artifacts

Artifact Location
bw_small.cu Custom 8MB CUDA bandwidth test
Bandwidth contention report DGX-Spark-Bandwidth-Contention-35B-vs-Idle.md
Kernel profiling report DGX-Spark-Kernel-Profile-35B-Analysis.md
Torch profiler trace vllm-trace.json.gz (25MB, viewable in perfetto.dev)
Raw profiler output vllm-profiler-raw.txt

Environment

  • vLLM v0.17.1.dev0+gb31e9326a (single-node) / v0.17.0 (4-node cluster)
  • PyTorch 2.10.0a0+a36e1d39eb.nv26.01.42222806
  • CUDA 13.0, Driver 580.126.09
  • NCCL 2.28.9-1 SM121
  • Profiler: PyTorch torch.profiler via vLLM /start_profile API

I think the CPU-side memory bandwidth is limited to about 120 GB/s for some unknown reason, while the GPU side can almost fully utilize the 273 GB/s.

Vulkan memtest report about 180-190GB/s

./target/aarch64-unknown-linux-gnu/release/memtest_vulkan 
https://github.com/GpuZelenograd/memtest_vulkan v0.6.0 by GpuZelenograd
To finish testing use Ctrl+C
TU: error: ../src/freedreno/vulkan/tu_knl.cc:387: failed to open device /dev/dri/renderD128 (VK_ERROR_INCOMPATIBLE_DRIVER)
MESA: error: Opening /dev/dri/card0 failed: Permission denied

1: Bus=0x01:00 DevId=0x2E12   92GB NVIDIA GB10
2: Bus=0x00:00 DevId=0x0000   122GB llvmpipe (LLVM 20.1.2, 128 bits)
                                                   Override index to test:1
TU: error: ../src/freedreno/vulkan/tu_knl.cc:387: failed to open device /dev/dri/renderD128 (VK_ERROR_INCOMPATIBLE_DRIVER)
MESA: error: Opening /dev/dri/card0 failed: Permission denied
Standard 5-minute test of 1: Bus=0x01:00 DevId=0x2E12   92GB NVIDIA GB10
          NO ERRORS           written:   86.2GB  176.8GB/s      checked:   90.0GB  191.7GB/s     00:00:00.957
          NO ERRORS           written:  172.5GB  177.2GB/s      checked:  180.0GB  192.5GB/s     00:00:02.866
          NO ERRORS           written:  517.5GB  176.9GB/s      checked:  540.0GB  192.5GB/s     00:00:08.597
          NO ERRORS           written: 2760.0GB  176.6GB/s      checked: 2880.0GB  192.6GB/s     00:00:39.174