Skip to content

English · Español

Lab 02 — Test de ancho de banda: H2D, D2H, D2D

Objetivo: medir el ancho de banda real de las transferencias de memoria host-a-device, device-a-host y device-a-device; comparar contra los picos teóricos de PCIe y HBM.

Tiempo estimado: 60–90 minutos.

Prerrequisito: lab/01-device-query.md completo. Los números de device_query.json son la línea base de comparación.


Lo que produces

experiments/23-device-profile/ (extiende lo que hizo el lab 01):

  • bandwidth_test.py.
  • bandwidth_test.json — throughputs medidos a múltiples tamaños.
  • bandwidth_test.png — plot, tamaño en x (log), throughput en y, tres curvas (H2D, D2H, D2D).
  • interpretation.md — qué te cuenta el plot.

Los kernels

Tres transferencias:

  1. H2D (host-to-device)cudaMemcpyAsync desde un buffer host pinned (cudaMallocHost) a un buffer device.
  2. D2H (device-to-host) — dirección inversa, los mismos buffers.
  3. D2D (device-to-device)cudaMemcpyAsync desde un buffer device a otro, mismo device.

Tamaños: al menos 12 puntos, espaciados en log desde 1 KiB a 1 GiB. Misma escala que el lab 01 de la Fase 1 (memcpy-bandwidth).

TODOs

Bloque A — escribir bandwidth_test.py

  • Reserva buffers host pinned (cupy.cuda.alloc_pinned_memory o vía cudaHostAlloc). La memoria pinned es necesaria para máximo ancho de banda H2D / D2H; las transferencias desde memoria host pageable van ~la mitad de rápido.
  • Reserva dos buffers device (1 GiB cada uno).
  • Para cada dirección de transferencia y cada tamaño:
  • Elige iters tal que el tiempo total de transferencia ≥ 100 ms.
  • Warm up: 3 iteraciones antes de cronometrar.
  • Sincroniza con cupy.cuda.runtime.deviceSynchronize() antes y después del bloque cronometrado.
  • Usa eventos de cupy.cuda.Stream para cronometrado más fino si puedes; time.perf_counter_ns después de sync también vale.
  • Calcula: throughput = bytes_per_iter × iters / elapsed_s / 1e9 [GB/s].
  • Registra en bandwidth_test.json:
    {
      "h2d": [{"size_KiB": 1, "throughput_GBs": ..., "iters": ..., "elapsed_s": ...}, ...],
      "d2h": [...],
      "d2d": [...]
    }
    

Bloque B — plot

  • matplotlib. x = tamaño (escala log, KiB), y = GB/s (lineal). Tres curvas con colores distintos y leyenda clara.
  • Anota los techos teóricos (líneas horizontales discontinuas):
  • Techo H2D / D2H: pico PCIe. Desde device_query.json: pcie_generation × pcie_lane_width / 8 GB/s (muy a grosso modo — PCIe 4.0 x16 = 32 GB/s, PCIe 5.0 x16 = 64 GB/s).
  • Techo D2D: ancho de banda HBM desde device_query.json (theoretical_hbm_bandwidth_gbs).

Bloque C — interpretar en interpretation.md

Tres párrafos:

  1. La asimetría H2D/D2H. A menudo están dentro del 10% una de otra si ambas direcciones usan el mismo enlace PCIe. Si son muy diferentes, algo es asimétrico (por ejemplo, calidad PCIe irregular, o una optimización unidireccional en el driver).
  2. El plateau D2D. ¿En qué tamaño satura D2D? Debería formar plateau al 50–80% de theoretical_hbm_bandwidth_gbs para tamaños > 1 MiB. (Los tamaños pequeños no pueden saturar porque no generan suficientes transacciones de memoria.) Si el plateau medido es <30% del teórico, algo falla — comprueba que la memoria pinned esté realmente pinned, comprueba que estés sincronizando bien.
  3. Ratio D2D vs H2D. D2D debería ser 30–60× más rápido que H2D en tamaños grandes (HBM bandwidth vs PCIe). Indica el ratio que mediste; compara con el ratio del datasheet.

Bloque D — actualización del manifest

Añade al manifest.json existente del lab 01 (o crea una sección nueva):

{
  ...,
  "bandwidth_measured": {
    "h2d_peak_GBs": null,
    "d2h_peak_GBs": null,
    "d2d_peak_GBs": null,
    "h2d_pinned_ratio_vs_pageable": null,
    "d2d_fraction_of_hbm_peak": null
  }
}

(Opcional: mide el ratio pageable-vs-pinned H2D como experimento adicional. Pageable debería ser ~½ de pinned.)

Restricciones

  • Solo memoria pinned para medidas de pico H2D / D2H. Las transferencias pageable van a ~la mitad del ancho de banda; eso es un experimento separado.
  • Sincroniza correctamente. Sin sync estás cronometrando el overhead del lanzamiento del kernel, no la transferencia.
  • Una dirección a la vez. H2D y D2H concurrentes (en streams distintos) duplican el throughput agregado; eso es un experimento separado. La Fase 23 mide de una en una.
  • GPU única. Las transferencias peer-to-peer multi-GPU (NVLink) son de la Fase 35.

Condiciones de parada

Listo cuando:

  1. bandwidth_test.json tiene las tres curvas con 12+ puntos cada una.
  2. bandwidth_test.png muestra claramente las tres curvas y las líneas de techo.
  3. El pico D2D está dentro del 30% del ancho de banda teórico de HBM.
  4. Los picos H2D / D2H están dentro del 30% del ancho de banda teórico de PCIe.
  5. interpretation.md responde las tres preguntas del Bloque C con mediciones.
  6. Manifest actualizado.

Trampas

  • Ruido a tamaños pequeños. A 1 KiB de transferencia, el overhead del lanzamiento del kernel es >50% del cronometrado. No esperes que la curva H2D suba suavemente desde 1 KiB; la curva estará planchada hasta ~64 KiB y entonces sube.
  • D2H más lento que H2D. A veces ocurre por asimetría upstream/downstream de PCIe en ciertas plataformas. Anótalo; no intentes "arreglarlo".
  • Thermal throttling de la GPU. Las series largas de benchmark pueden throttlear. Mantén las mediciones individuales cortas (menos de 5 segundos cada una).
  • cudaMemcpy no es realmente async. Es síncrono si cualquiera de los buffers es pageable (no pinned). La versión async requiere ambos pinned.

Cuándo consultar solutions/

Tras cumplir las condiciones de parada. La referencia en solutions/02-bandwidth-test-ref.md (escrita al abrir la fase) compara contra los números esperados de la GPU elegida.


Siguiente lab: lab/03-gpu-roofline.md.