Cómputo Distribuido — Introducción Conceptual y Síntesis
Los modelos M1–M5 operan dentro de una sola máquina. El cómputo distribuido rompe esa frontera: múltiples máquinas físicamente separadas cooperan para resolver un problema. Este archivo es conceptual — la implementación real (Ray, Dask, sistemas de mensajería) es tema de un módulo posterior.
Modelo 6 — Distribuido (M6)
En la cocina
N restaurantes en ciudades distintas — cada uno con su propia despensa, su propia cocina, su propio equipo y sus propios relojes de pared. Un pedido que llega al restaurante de CDMX no puede acceder directamente a los ingredientes del restaurante de GDL. La comunicación entre restaurantes es por mensajero físico: tarda tiempo real en llegar, y puede llegar tarde, en desorden, o perderse.
Esto es radicalmente diferente de tener múltiples estaciones en la misma cocina (M5): allí todos comparten el mismo espacio y se comunican al instante.
En lenguaje natural
Múltiples máquinas físicamente separadas, cada una con su propia memoria, coordinadas por red. La comunicación tiene latencia no despreciable. No existe un reloj global que sincronice los eventos de todas las máquinas.
Formalmente
M6 — Distribuido:
N ≥ 2 nodos (máquinas físicamente separadas)
Para todo nᵢ, nⱼ ∈ Nodos, i ≠ j:
(1) Mem(nᵢ) ∩ Mem(nⱼ) = ∅ no hay memoria compartida entre nodos
(2) δᵢⱼ > 0 latencia de red no despreciable
(3) ∄ reloj global no existe un reloj único y sincronizado
La analogía mapeada formalmente
Ciudad separada ↔ nodo físico ↔ nᵢ ∈ Nodos
Despensa propia ↔ RAM del nodo ↔ Mem(nᵢ), aislada
Mensajero entre ciudades ↔ mensaje de red ↔ msg: nᵢ → nⱼ
Tiempo de viaje ↔ latencia de red ↔ δᵢⱼ > 0
Relojes de pared distintos ↔ sin reloj global↔ ∄ t_global
Las tres diferencias con M5 (paralelo)
| Eje | Paralelo (M5) | Distribuido (M6) |
|---|---|---|
| Ubicación | Misma máquina física, mismo OS | Máquinas físicamente separadas |
| Latencia de comunicación | IPC del OS (pipe, queue): δ ≈ μs | Red (HTTP, gRPC, TCP): δ ≈ ms a s |
| Reloj | Reloj de hardware compartido | Relojes locales independientes |
Nota: en Python,
multiprocessingya usa procesos con memoria aislada (Mem(pᵢ) ∩ Mem(pⱼ) = ∅). La diferencia entre paralelo y distribuido NO es “memoria compartida vs no” — es latencia de comunicación y ubicación física.
El problema del reloj global
En una sola máquina, existe un reloj de hardware que todos los procesos comparten. El OS puede ordenar eventos con precisión de nanosegundos.
En un sistema distribuido, cada nodo tiene su propio reloj — y estos relojes divergen inevitablemente:
- Osciladores de cuarzo con frecuencias ligeramente distintas
- Temperatura y voltaje afectan la velocidad del reloj
- NTP (Network Time Protocol) sincroniza aproximadamente, con error de ms
El problema concreto con el chatbot v4
Servidor CDMX (reloj local: 10:00:00.001):
Petición de usuario_A llega
timestamp_local = "10:00:00.001"
Servidor GDL (reloj local: 10:00:00.000 — 1ms adelantado):
Petición de usuario_B llega
timestamp_local = "10:00:00.000"
¿Cuál llegó primero?
Por timestamps locales: usuario_B (GDL) parece anterior
Por realidad física: puede haber llegado después
No podemos saber cuál llegó primero solo comparando timestamps locales de servidores distintos. Esto importa para: ordenamiento de transacciones, consistencia de historial de chat, debugging de eventos distribuidos.
La solución conceptual: Relojes Lógicos
Sin reloj global, los sistemas distribuidos usan relojes lógicos para ordenar eventos causalmente:
-
Relojes de Lamport: cada nodo mantiene un contador entero. Al enviar un mensaje, incluye su contador. Al recibir, el receptor actualiza al máximo + 1. Garantiza: si A causó B, entonces timestamp(A) < timestamp(B).
-
Relojes Vectoriales: cada nodo mantiene un vector de contadores (uno por nodo). Permiten detectar concurrencia real entre eventos: dos eventos son concurrentes si ninguno causó al otro.
Chatbot v4 — el sistema distribuido
En la cocina
La cadena de restaurantes tiene una sede central de pedidos online que distribuye las órdenes entre sucursales. Cada sucursal procesa las órdenes de forma independiente. Hay un sistema de mensajería entre sucursales para coordinar pedidos grandes. Cada sucursal escala por sí sola añadiendo más cocineros.
Arquitectura
Internet
Usuarios ────────────→ Balanceador de carga (nginx / cloud LB)
│ │
┌────────┴──┐ ┌────┴────────┐
│ Servidor │ │ Servidor │
│ CDMX │ │ GDL │
│ (v3: M5b) │ │ (v3: M5b) │
└────┬──────┘ └──────┬──────┘
│ │
┌────┴─────────────────┴────┐
│ Cola de mensajes │
│ (Redis / RabbitMQ) │
└────────────────────────────┘
│
┌─────────┴────────────────┐
│ Workers LLM │
│ (GPUs en la nube) │
└──────────────────────────┘
Propiedades formales del sistema:
Cada servidor: Mem(CDMX) ∩ Mem(GDL) = ∅ ← sin memoria compartida
Comunicación: δ_CDMX↔GDL > 0 ← latencia de red real
Escalado: agregar servidores sin límite de cores (vs Amdahl en M5)
El balanceador distribuye peticiones entre instancias. Las colas de mensajes desacoplan servidores de workers GPU. Cada componente escala independientemente.
Scope de este módulo: entender conceptualmente qué es M6 y por qué es diferente de M5. La implementación real (Ray, Dask, Celery, Kafka) requiere infraestructura específica — tema de un módulo posterior.
La evolución del chatbot: v1 → v4
Chatbot v1 — Secuencial (M1)
Un usuario a la vez
T_total = N × T_usuario
Latencia con 100 usuarios: 155s [Escenario A]
↓ problema: CPU idle durante I/O (99.94% del tiempo)
Chatbot v2 — Concurrent + Async (M4) [Escenario A optimizado]
N usuarios simultáneos con asyncio
T_total ≈ T_max_usuario ≈ 1.55s
↓ pregunta: ¿y si el LLM es local? → inferencia bloquea el event loop
Chatbot v3 — Parallel + Async (M5b) [Escenario B]
asyncio para I/O + ProcessPoolExecutor para inferencia
Speedup de inferencia ≈ P (limitado por Amdahl)
↓ límite: una máquina tiene P cores finitos
Chatbot v4 — Distribuido (M6)
Múltiples servidores + balanceador + cola de mensajes
Escalado horizontal: agregar máquinas sin límite de cores
Desafíos: consistencia, latencia de red, ausencia de reloj global
Síntesis final: los 6 modelos
| Modelo | Condición formal clave | Analogía | Escenario chatbot | Librería principal |
|---|---|---|---|---|
| M1 Secuencial | end(τᵢ) ≤ start(τⱼ) | Cocinero: una orden completa antes de la siguiente | v1: 1 usuario a la vez | (ninguna) |
| M2 Async no concurrent | wait≠∅ pero exec(τⱼ) ∩ wait(τᵢ) = ∅ | Cocinero parado frente al horno | Anti-patrón: await fn1(); await fn2() |
asyncio (mal usado) |
| M3 Concurrent no async | Solapamiento, wait=∅, P=1 | Turnos con los cuchillos únicos | CPU-bound con threading — evitar | threading (ineficiente) |
| M4 Concurrent + async | exec(τⱼ) ∩ wait(τᵢ) ≠ ∅, H=1 | Lista de pendientes, aprovecha el horno | v2: Escenario A, LLM API | asyncio |
| M5a Paralelo puro | ∃t: exec(τᵢ) ∩ exec(τⱼ) ≠ ∅, wait=∅ | Dos estaciones CPU-bound | Procesamiento numérico masivo | ProcessPoolExecutor, joblib |
| M5b Paralelo + async | ∃t: exec(τᵢ) ∩ exec(τⱼ) ≠ ∅, wait≠∅ | Dos estaciones con hornos, coordinadas | v3: Escenario B, LLM local | asyncio + ProcessPoolExecutor |
| M6 Distribuido | N≥2 nodos, Mem∩Mem=∅, δ>0, ∄ reloj | N restaurantes en ciudades distintas | v4: múltiples servidores | Ray, Dask, Celery |
La jerarquía
Distribuido ⊇ Paralelo ⊇ Concurrente (jerarquía de alcance)
Asíncrono: ortogonal — se combina con cualquier nivel
Dentro de Python:
Concurrente (M3, M4): mismo proceso, mismo OS
Paralelo (M5): múltiples procesos, misma máquina
Distribuido (M6): múltiples máquinas, red de por medio
Regresa a la pregunta de 01_procesos_y_hilos.md: “si tu chatbot recibe 10 peticiones simultáneas y cada una tarda ~1.55s, ¿cuánto tiempo espera el último usuario?”
Ahora responde para los 4 modelos:
- v1 (M1): latencia del usuario 10 — justifica con end(τᵢ) ≤ start(τᵢ₊₁)
- v2 (M4, Escenario A): latencia del usuario 10 — justifica con exec(τⱼ) ∩ wait(τᵢ) ≠ ∅
- v3 (M5b, Escenario B): si la inferencia tarda 2s y tienes P=4 cores, ¿cuál es el speedup esperado con S≈0.05?
- v4 (M6): con 3 servidores v3 en el balanceador, ¿cuántos usuarios concurrentes puedes manejar antes de degradar?