Skip to content

English · Español

Lab 01 — Strides y vistas

Objetivo: ver que arr.T es O(1) y que np.ascontiguousarray(arr.T) es O(n), por medición. Producir, intencionadamente, un bug de aliasing. Crear memoria muscular para flags.OWNDATA.

Tiempo estimado: 45–60 minutos.

Prerrequisitos: lab 00.


Lo que produces

Un directorio experiments/06-strides-and-views/ que contiene:

  • measure.py — tu script de benchmark.
  • aliasing.py — un script de 20 líneas que produce el bug de aliasing intencionadamente e imprime pruebas.
  • results.json — tiempos medidos para arr.T frente a np.ascontiguousarray(arr.T) frente a arr.copy() a lo largo de varios tamaños.
  • transpose-cost.png — gráfico log-log de tiempo frente a tamaño del array para las tres operaciones.
  • manifest.json{seed, versions, config, hardware}.
  • README.md (3–4 párrafos) — qué mediste, qué demostró el bug de aliasing, tres frases de interpretación.

TODOs

Bloque A — medir transpuesta frente a transpuesta-contigua

  • Reserva a = np.random.default_rng(42).standard_normal((N, N), dtype=np.float32) para N ∈ {64, 128, 256, 512, 1024, 2048, 4096} (7 tamaños).
  • Para cada N, cronometra tres operaciones (repite cada una ≥10× tras un warm-up):
  • b = a.T (solo reetiquetar)
  • c = np.ascontiguousarray(a.T) (copia con nuevo layout)
  • d = a.copy() (copia sin transponer — debería igualar (2) asintóticamente)
  • Registra N, n_bytes (= 4·N²), op, time_ns_per_call. Guarda en results.json.
  • Asegura en tu script: b.base is a, c.base is not a, c.flags.C_CONTIGUOUS is True, b.flags.C_CONTIGUOUS is False.

Bloque B — gráfico

  • matplotlib. Eje x: n_bytes en MiB (escala log). Eje y: tiempo en ms (escala log).
  • Tres líneas: a.T, ascontiguousarray(a.T), a.copy(). Marcadores + líneas.
  • a.T debería ser plana cerca de 1 μs (independiente de N). Las otras dos deberían crecer linealmente con n_bytes.
  • Guarda como transpose-cost.png.

Bloque C — bug de aliasing, a propósito

En aliasing.py:

  • Reserva a = np.arange(20, dtype=np.int32).
  • Toma b = a[::2] (cada elemento alterno).
  • Asegura b.flags.OWNDATA is False y b.base is a.
  • Muta: b[0] = -999.
  • Imprime a — muestra que a[0] es ahora -999.
  • Luego muestra el patrón seguro: c = a[::2].copy(). Muta c. Muestra que a no se ve afectado.
  • Escribe un comentario de 1 frase al inicio de aliasing.py describiendo el bug.

Bloque D — demo de as_strided (opcional, exploratorio)

Si tienes tiempo, usa numpy.lib.stride_tricks.sliding_window_view para crear una vista de ventana deslizante de longitud 3 sobre un array de 10 elementos. Imprímela. Confirma vía np.shares_memory que comparte el buffer base. No escribas en ella.

Bloque E — manifest

Esquema estándar (ver lab 00 para la plantilla). Incluye en config:

"sizes_N": [64, 128, 256, 512, 1024, 2048, 4096],
"ops": ["transpose", "ascontiguousarray_T", "copy"],
"warmup_iters": 1,
"min_repeats": 10

Restricciones

  • fp32, no fp64. Queremos coincidir con el dtype de tensores de fases posteriores.
  • Misma semilla de RNG para todos los tamaños. Reproducibilidad.
  • CPU governor performance. Como en el lab de Fase 1.
  • Sin threading. Medición monohilo.

Resultados esperados (aproximados, para el i5-8250U)

Op N=1024 N=4096
a.T ~1 μs ~1 μs
np.ascontiguousarray(a.T) ~3 ms ~70 ms
a.copy() ~2 ms ~30 ms

La primera fila debería ser plana. Las otras dos deberían escalar aproximadamente con . ascontiguousarray(a.T) es más lento que un copy() simple porque la transpuesta recorre el buffer en orden de strides no-unitarios (mal comportamiento de caché).

Condiciones de parada

Hecho cuando:

  1. transpose-cost.png muestra el contraste plano-frente-a-lineal claramente.
  2. aliasing.py corre de principio a fin y el array a impreso refleja la escritura a través de b.
  3. README.md interpreta la forma de la curva (una frase por línea: "a.T es plana porque…", "ascontiguousarray(a.T) escala linealmente porque…", "copy() es ligeramente más rápido porque…").

Escollos

  • Warmup de JIT. La primera llamada a np.ascontiguousarray puede incluir el tiempo de carga de librerías. Corre un warm-up antes del bucle de cronometraje.
  • GC durante el cronometraje. Envuelve los bloques cronometrados con gc.disable() / gc.enable() para repetibilidad.
  • Números engañosos en N grandes por swap. Un fp32 de 4096×4096 son 64 MiB — bien en 62 GiB de RAM. Pero si subes a N=16384 (1 GiB), asegúrate de no estar tocando swap. Vigila free -h.
  • b = a.T; b[0, 0] = 99 debería mutar a. Si no lo hace, tomaste una copia accidentalmente en algún sitio. Comprueba b.base.
  • Olvidar la prueba de OWNDATA. El objetivo del bloque de aliasing es hacer el bug visible. Imprime b.flags antes de la mutación.

Cuándo consultar solutions/

Después de que tu transpose-cost.png exista, tu aliasing.py corra y tu README.md tenga las tres frases de interpretación. Entonces solutions/01-strides-and-views-ref.md (escrito en la apertura de fase) muestra los tiempos de referencia y el recorrido canónico del bug de aliasing.


Siguiente lab: lab/02-broadcasting-trap.md.