. Los agentes de producción pelean por la misma GPU, y en una tarjeta compartida, la latencia p99 de un agente sensible a la latencia empeoró silenciosamente un 66 % mientras que todos los pods aún se reportaban en buen estado. Esto es lo que realmente cuesta esa pelea, medido al p99, no agitado con la mano.
Esta es la Parte 2 de la serie “Inferencia agente de grado de producción”. Cada parte elimina un tipo de trabajo redundante de un proceso de LLM agente. La parte 1 elimina el precarga redundante. La parte 2 (esta parte) aborda la espera redundante: cómo varios microagentes comparten una GPU mediante la división del tiempo. La parte 3 mantiene la recuperación de RAG en la GPU con un kernel CUDA Top-K personalizado. La parte 4 mantiene el estado del agente durante las transferencias para que el siguiente agente nunca tenga el problema de arranque en frío.
Conclusiones clave
Compartir una GPU no es gratis y su programador no se lo dirá. Cuando dos agentes comparten una GPU con intervalos de tiempo, Kubernetes felizmente informa que ambos pods están en ejecución. El daño se esconde en la cola de latencia. La mediana miente; la cola dice la verdad. En mi carrera (con solo 2 agentes), ambos mantuvieron un p50 casi sin cambios. Pero el p99 del pequeño y sensible a la latencia saltó de 3,68 ms a 6,10 ms (≈1,66×) y su jitter (p99/p50) pasó de 1,02 a 1,70. El agente sensible a la latencia se degrada primero. La carga de trabajo pequeña y nerviosa sufrió mucho más que la pesada y constante, a pesar de que ambas "consiguieron una GPU". El rendimiento apenas se movió, lo cual es toda la trampa. Un proxy de rendimiento de tasa media disminuyó solo un pequeño porcentaje, por lo que un panel que observe los promedios consideraría que esto es un éxito, mientras que su agente sensible a la cola incumple silenciosamente una fecha límite entre cincuenta. Funciona con una GPU de 150 dólares. Todo lo que se muestra a continuación se mide en una sola GTX 1080 de cinco años con el complemento de dispositivo NVIDIA Kubernetes y la división de tiempo CUDA. Sin H100, sin MIG, sin magia. Esto fue intencionado, no todo el mundo puede permitirse el lujo de H100; algunos todavía siguen usando su antiguo hardware. Y, sinceramente, ejecutar una producción de IA agente en H100 no requiere ningún tipo de magia; pero en una GPU de $150, seguramente sí.
TL;DR: Puse dos cargas de trabajo de agente muy diferentes (un trabajador FFT pequeño y sensible a la latencia y un trabajador GEMM pesado de estilo transformador) en pods de Kubernetes separados, cada uno de los cuales solicitó cortésmente nvidia.com/gpu: "1" y dejé que el corte de tiempo CUDA del complemento del dispositivo NVIDIA los colocara a ambos en una GTX 1080 física. Luego cronometré cada iteración con eventos CUDA y los integré en p50/p95/p99, calculó un factor de degradación (cola compartida/cola individual) y lo cotejó con los contadores de utilización de GPU DCGM. Resultado: las medianas y el rendimiento apenas variaron, pero la latencia de cola y la inquietud aumentaron, lo peor para el agente pequeño y de latencia crítica. Kubernetes dice "dos vainas sanas". El silicio dice "uno de ustedes se muere de hambre en la cola". Kubernetes informa "dos vainas sanas". El silicio informa de una pelea callejera entre los buses de memoria y la cola p99 te dice quién pagó el precio.
Repositorio de Github: https://github.com/AnubhabBanerjee/Kube-Timeslice-Profiler
(Confesión rápida antes de comenzar: llegué a esto con experiencia en ingeniería de RAN 5G/6G. Resulta que es exactamente el tipo de problema que enfrenta AI RAN actualmente. En los servidores de borde, los operadores están tratando de ubicar el procesamiento de banda base de latencia crítica con una gran inferencia de LLM en las mismas GPU. Se convierte en una pesadilla de programación en el momento en que la carga de trabajo de IA comienza a privar de hambre a las aplicaciones de ancho de banda de memoria de latencia crítica, y es exactamente por eso que escribí esto. publicación.)
Modelo mental de arquitectura: mantén esto abierto mientras lees.
Dos pods → cada uno solicita nvidia.com/gpu: 1 → el complemento del dispositivo dice alegremente "claro, aquí hay 4 GPU" (hay exactamente 1) → CUDA divide el tiempo en la única GPU real → todos se turnan → la cola paga la cuenta.
Todo lo que aparece a continuación es solo un comentario sobre una parte de esa línea.
1. Una confesión: “Correr” es la ilusión más cara de Kubernetes
Al igual que en la publicación anterior de esta serie, comencemos con una conversación dramática antes de sumergirnos lentamente en temas técnicos más aburridos.
Tú: "Kubernetes, ejecuta a mis dos agentes".
Kubernetes: "Listo. Ambos pods se están ejecutando. ✅"
Tú: "¿En la misma GPU?"
Kubernetes: "Sí. Cada uno pidió nvidia.com/gpu: 1, así que le di a cada uno una GPU".
Tú: "Pero solo tengo una GPU".
Kubernetes: "Correcto. Y les di a cada uno una GPU". 🫡
Tú: "Espera, ¿¡qué!? ¿Cómo?? No pueden ambos tener…"
Kubernetes: "Shhh. No te preocupes. Mira qué verdes son".
Su panel de Grafana: "Todo se ve bien, hermano. 🟢"
Mientras tanto…
Tu GPU física: (gritando en cambios de contexto)
Tu latencia p99: (doblándose silenciosamente en la esquina)
Bueno, tal vez no fue tan dramático después de todo, pero entiendes mi punto, ¿verdad? La idea de “salud” del programador es que el módulo está activo y se está ejecutando un proceso. No tiene opinión sobre si su agente de latencia crítica está siendo expulsado de la GPU cuarenta veces por segundo. La fase Pod dice Corriendo. El agente no dice nada, porque, bueno, en realidad nadie preguntó.
Esto sigue directamente desde donde terminó la Parte 1. En la publicación de SwarmKV, tenía dos agentes leyendo un documento y me jactaba de haberlo rellenado previamente una vez y desplegado el caché de KV. Luego, en las advertencias, admití la parte vergonzosa: el trabajo real de GPU de cada rama todavía se ejecutaba detrás de un mutex global. La orquestación se desplegó; El cómputo se alineó en una sola fila. Dos agentes, dos turnos. Cincuenta agentes, cincuenta turnos. Enrollé un candado a mano y terminé el día.
Eso está bien para una demostración. Es un desastre para la producción, donde “un enjambre de agentes” significa una docena de pequeños modelos especializados (un enrutador, un resumidor, un verificador de seguridad, un recuperador, un montón de llamadores de herramientas) todos despiertos a la vez, todos queriendo el mismo acelerador. No puedes comprarles un H100 a cada uno de ellos (a menos que tu nombre sea Jensen Huang). Los empaquetas en una GPU compartida y esperas que el programador lo solucione.
Entonces quería responder una pregunta contundente: cuando dos agentes comparten una GPU, ¿cuánto paga realmente cada uno? ¿Algo en mi clúster me lo dirá?
Alerta de spoiler: cuesta milisegundos reales, recae casi por completo en el pequeño agente rápido y no, nada en su clúster se lo dirá. Entonces construí una herramienta que lo hace.
2. Dos agentes con personalidades opuestas
El repositorio detrás de esta publicación ejecuta dos trabajadores de PyTorch en contenedores que representan los dos tipos que se encuentran básicamente en casi todos los enjambres de agentes:
Un agente pequeño, nervioso y sensible a la latencia (fft_worker.py). Ejecuta un bucle continuo de grandes FFT complejas en 2-D. Piense en ello como la clase de enrutador / barandilla / llamador de herramientas: los agentes que deben responder ahora o el mundo entero comenzará a desmoronarse. Un agente grande, estable y ávido de computación (matmul_worker.py). Ejecuta un flujo continuo de grandes multiplicaciones de matrices cuadradas: el GEMM en el corazón del paso directo de un transformador. Este es el peso pesado que realmente hace pensar al modelo.
Toda su carga de trabajo es bastante sencilla para cada uno. El trabajador de FFT preasigna un tensor complejo de 4096 × 4096 y lo golpea:
# —– Preasignación de tensores —– # La asignación única mantiene la creación del plan cuFFT y el tráfico del asignador fuera de la ventana de “elapsed_time“ por iteración en la GPU. # “complex64“ coincide con el ancho de datos típico de PHY IQ; FFT solo real subestimaría el tráfico de memoria relevante para la disputa de DRAM con los inquilinos de GEMM. data = torch.randn(MATRIX_SIZE, MATRIX_SIZE, dispositivo=dispositivo, dtype=torch.complex64) # Los primeros lanzamientos pagan costos JIT/plan; cinco iteraciones es un conteo fijo pequeño; el recorte formal en estado estacionario todavía ocurre en “generate_results“ §1.4. # El “fft2“ desechable llama a instrucciones principales y cachés constantes para que las iteraciones cronometradas vean una ocupación SM repetible, no picos de un solo disparo del conductor. for _ in range(5): # La asignación a “_“ descarta inmediatamente el identificador del tensor de salida; sólo necesitamos efectos secundarios de ejecución del kernel en los “datos“ residentes en el dispositivo. torch.fft.fft2(data) # La sincronización final garantiza que el kernel de calentamiento no se superponga al par de eventos de la primera iteración cronometrada, fundamental para la validez del tiempo de eventos CUDA §3. sincronización()
El trabajador GEMM preasigna dos matrices FP32 y las multiplica por siempre:
# Matmul necesita dos operandos residentes en el dispositivo; la asignación una vez mantiene al asignador y a la paginación fuera de la ruta cuBLAS cronometrada en cada iteración. # FP32 es el tipo de entrenamiento/inferencia predeterminado en GPU de clase Pascal sin Tensor Cores; esto coincide con la narrativa "GEMM en 1080" en README. A = torch.randn(MATRIX_SIZE, MATRIX_SIZE, dispositivo=dispositivo) B = torch.randn(MATRIX_SIZE, MATRIX_SIZE, dispositivo=dispositivo) # El autoajuste de cuBLAS puede elegir diferentes algoritmos en los primeros lanzamientos; las iteraciones de calentamiento absorben ese no determinismo antes de las líneas “KTS_APP“. # Cinco repeticiones reflejan al trabajador FFT, por lo que las comparaciones entre inquilinos en los artículos no confunden diferentes profundidades de calentamiento con efectos de interferencia del silicio. para _ en rango(5): # Resultado descartado; La memoria máxima permanece plana porque el tensor de salida se libera en cada iteración antes de que el bucle cronometrado asigne nada nuevo por iterador. torch.matmul(A, B) # Sync cierra la ventana de calentamiento para que el primer “_ev_start.record“ no se superponga a los núcleos de calentamiento finales en la misma semántica de flujo CUDA predeterminada. sincronización()
El objetivo nunca fue construir un modelo inteligente, sino construir dos ciudadanos de la GPU con modales opuestos y verlos compartir una habitación. Se termina en unos 3,6 ms y se quiere volver inmediatamente; el otro tarda unos 20 ms y sólo quiere moler. Ahora colócalos en la misma GPU y haz la única pregunta interesante: ¿quién parpadea primero?
Ambos trabajadores están configurados por variables de entorno, por lo que una especificación de pod puede volver a ajustarlos sin reconstruir la imagen:
# —– Configuración (anulable mediante variables de entorno para que las especificaciones del pod puedan ajustarse por experimento) —– # “ITERACIONES“ coincide de forma predeterminada con el trabajador FFT para que los numeradores/denominadores de DF utilicen recuentos de muestras comparables sin anulaciones de entorno en YAML. # Aumentar las iteraciones alarga la “kubectl wait“ de la GPU compartida; Reducir la variación de los picos en las colas de p99 utilizadas para la narración de conflictos en “results.md“. ITERACIONES = int(os.environ.get("ITERACIONES", 800)) # “MATRIX_SIZE“ domina los FLOP por iteración; La anulación de env le permite reducir la VRAM cuando MatMul comparte 8 GB con asignaciones de co-inquilino de FFT. # La división de tiempo no divide la memoria: las asignaciones máximas de ambos pods deben caber en una tarjeta física o la ruta OOMKill más lenta invalidará el experimento. MATRIX_SIZE = int(os.environ.get("MATRIX_SIZE", 4096)) # “SLEEP_MS“ tiene un valor predeterminado ligeramente superior a los 100 ms de FFT, por lo que dos inquilinos rara vez se despiertan al mismo tiempo, extendiendo los cuantos del programador para una interferencia más realista. # Misma advertencia que FFT: la suspensión se realiza entre iteraciones medidas y está excluida de “latency_ms_device“; solo el tiempo matmul de GPU está en la lista de muestra. SLEEP_MS = int(os.environ.get("SLEEP_MS", 150))
Supongo que a estas alturas te darás cuenta de que nada aquí es específico de un dominio. Resulta que los números provienen de una carga de trabajo de procesamiento de señales junto a un matmul, pero intercambie sus dos agentes (uno liviano y que cumple con los plazos, otro pesado y constante) y la historia se mantiene. Esta es una publicación sobre personalidades de cargas de trabajo que chocan en un acelerador, no sobre una aplicación en particular.
Sincronizándolo sin dejarse engañar
Existe una forma clásica de comparar una GPU y obtener un número hermoso pero completamente incorrecto: medir cuánto tiempo le toma a Python iniciar el kernel. CUDA es asíncrono, por lo que torch.matmul(A, B) regresa casi instantáneamente mientras la GPU todavía está sudando. Mide eso y estarás satisfecho de que tu matmul toma solo 50 microsegundos, y luego comenzarás a golpearte la cabeza preguntándote por qué la producción es lenta.
Los trabajadores no hacen eso. Envuelven cada operación en eventos CUDA y fuerzan un torch.cuda.synchronize() para que el reloj se detenga después de que los núcleos realmente se retiren en los SM:
# La época de inicio inmediatamente antes del "registro" minimiza la brecha entre la "intención de lanzamiento" y el envío de la cola para los estudios de alineación de unión. epoch_ns_start = time.time_ns() _ev_start.record() _ = torch.fft.fft2(data) _ev_end.record() torch.cuda.synchronize() epoch_ns_end = time.time_ns() latency_ms_device = float(_ev_start.elapsed_time(_ev_end))
elapsed_time lee la propia línea de tiempo de la GPU: resolución inferior a microsegundos, sin fluctuaciones del lado del host. Esa sincronización() es la diferencia entre medir "cuánto tiempo funcionó la GPU" y "cuánto tiempo tardó Python en preguntar". Luego, cada iteración genera una línea estructurada y la vacía, de modo que la transmisión de registros de Kubernetes la ve inmediatamente:
print( f"KTS_APP,v1,FFT,{i},{epoch_ns_start},{epoch_ns_end},{latency_ms_device:.6f},{phase_optional}" ) # “flush“ fuerza la salida estándar del contenedor con búfer de línea a través de CRI antes del siguiente modo de suspensión; sin él, tail -f puede agrupar líneas y codificar el orden de unión. sys.stdout.flush()
El tiempo de ejecución del silicio bruto aumenta; Sale un registro estructurado. Un analizador posterior los agrega en percentiles exactos, creando un contrato de medición estricto que elimina todo el ruido del lado del host.
3. Cómo terminan dos pods en una GPU (explicación)
Esta es la parte que les parecerá mágica a las personas que son nuevas en el K8. Para otros, pueden saltarse esta sección con seguridad y pasar a la siguiente.
De forma predeterminada, Kubernetes trata nvidia.com/gpu como un todo e indivisible: una GPU, un reclamante, sin compartir. La función de división de tiempo del complemento del dispositivo NVIDIA cambia la contabilidad. Le entregas un ConfigMap que dice, esencialmente, "imagina que cada GPU física es varias":
apiVersion: v1 tipo: ConfigMap metadatos: nombre: time-slicing-config espacio de nombres: nvidia-device-plugin datos: cualquiera: |- versión: v1 banderas: migStrategy: "ninguno" failOnInitError: verdadero compartir: timeSlicing: failRequestsGreaterThanOne: verdadero renameByDefault: falso recursos: – nombre: nvidia.com/gpu réplicas: 4
réplicas: 4 es Kubernetes para "mentirle al programador cuatro veces". Después de esto, una GTX 1080 física anuncia cuatro ranuras nvidia.com/gpu asignables a la API. Cuatro pods pueden solicitar cada uno "1" y todos se programan, muy felizmente.
Aquí está el truco, en negrita porque toda la publicación depende de ello: esto no divide físicamente el hardware. No es MIG. No hay barrera de memoria ni barrera de cómputo. Las cuatro “GPU” son del mismo silicio, y las cápsulas se turnan a través de la división de tiempo CUDA: el contexto de la GPU cambia entre ellas como un solo barista que atiende cuatro líneas corriendo entre cajas. Más espacios programables, aislamiento exactamente cero.
El experimento consta de tres trabajos de Kubernetes: cada agente solo (las líneas de base) y luego ambos a la vez. El manifiesto de "ambos a la vez" es todo el juego: dos trabajos, cada uno de los cuales solicita inocentemente una GPU, y aterrizan deliberadamente en la misma tarjeta:
contenedores: – nombre: trabajador imagen: localhost/kts-worker:v1 imagePullPolicy: nunca recursos: límites: nvidia.com/gpu: "1" solicitudes: nvidia.com/gpu: "1"
Ninguno de los grupos sabe que el otro existe. Ninguno pidió compartir. El planificador los puso en la misma habitación porque, hasta donde sabe, había cuatro habitaciones. Las líneas de base indican qué tan rápido se ejecuta cada agente cuando posee la GPU; la ejecución compartida le indica lo que paga la empresa. La brecha entre ellos es toda la historia.
4. La plataforma, en una frase
Todo lo que aparece a continuación se ejecuta en una NVIDIA GTX 1080 (8 GB, Pascal) de siete años de antigüedad en un K3 de un solo nodo con el complemento de dispositivo NVIDIA original y división de tiempo CUDA. Ni H100, ni MIG, ni rack de centro de datos: solo la tarjeta que la mitad de las personas que leen esto todavía tienen debajo de su escritorio.
Estoy usando esta antigüedad a propósito. La mala programación no desaparece mágicamente en un H100; simplemente ejecuta sus cuellos de botella a una velocidad de reloj más alta. Si sus agentes están peleando por un bus de memoria en una tarjeta de 150 dólares, invertir 30.000 dólares en el problema no evitará el atasco: sólo encarecerá el accidente. Lanzar un H100 a un defecto de orquestación no soluciona el conflicto; simplemente te permite ejecutar una mala arquitectura en menos milisegundos. A la física del desalojo de caché no le importa en qué año se acuñó su silicio.
(Las versiones del controlador, del contenedor y del kit de herramientas están fijadas en el repositorio para cualquiera que las reproduzca; son aburridas a propósito y no las necesitas para seguir la historia).
5. Los recibos (es decir, los números)
Ahora toda la historia en una imagen:
Cuatro paneles, un remate. Las medianas (el par de barras de la izquierda en cada gráfico de latencia) están básicamente intactas. Los rendimientos (fila inferior) perdieron un miserable 7,3% y 1,4%, el tipo de número que reportarías en la cadena y por el cual obtendrías un emoji de aprobación. Y luego está la esquina superior derecha del gráfico superior izquierdo: el p99 del agente pequeño aumentó un 66%. El mismo panel, los mismos módulos de ejecución, el mismo aburrido gráfico de rendimiento, y uno de sus dos agentes ahora es ocasionalmente, de manera impredecible, un 66 % más lento que ayer. Bienvenido a compartir GPU.
Los números reales, para que nadie tenga que entrecerrar los ojos ante las barras:
Lea esas filas FFT dos veces. La mediana no se movió. Si estuvieras mirando el tablero de un p50, jurarías que no pasó nada, cerrarías la sesión y te irías a almorzar. Pero una de cada cien llamadas FFT ahora demora un 66% más, y la brecha entre una iteración típica y una mala casi se duplicó. En promedio, no ralentizaste al agente; lo hiciste ocasionalmente, de manera impredecible. Lo que es peor, porque ahora es un agente escamoso y nadie puede reproducirlo un viernes por la tarde.
Ésta es la asimetría clave, y no es una coincidencia: el agente pequeño y sensible a la latencia se degrada primero y peor. El gran GEMM es una topadora: toma su cuanto y avanza. Al pequeño FFT lo siguen golpeando en el hombro a mitad de su zancada, lo empujan fuera de los SM y le dicen que espere su siguiente turno. Cuando dos cargas de trabajo comparten una sola línea, la que necesitaba ser rápida es la que sufre. Esto tiene enormes implicaciones en el ámbito de las telecomunicaciones: si esto continúa sucediendo, las llamadas comienzan a caer y, en el peor de los casos, incluso los números de los servicios de emergencia también pueden dejar de funcionar. ¡Deja que ese pensamiento penetre!
Para que esto sea comparable entre cualquier par de agentes, la herramienta calcula un factor de degradación (DF) = share_p99 / baseline_p99. DF = 1,0 significa que compartir fue gratis. Más alto significa que duele. Para esta ejecución es 1,66 para FFT y 1,18 para GEMM. Ese 1,66 es la publicación completa comprimida en un número que puede colocar en una diapositiva para mostrársela a su gerente.
Y aquí está la parte que debería ser ilegal: el rendimiento apenas se movió. Si su SLO (objetivo de nivel de servicio) está escrito en términos de rendimiento promedio, miraría “FFT bajó un 7%, GEMM bajó un 1%” y declararía la victoria. Mientras tanto, su sensible agente está incumpliendo silenciosamente una fecha límite de cada cincuenta. Los promedios son donde se esconde la discordia. El malo es un alma bondadosa que supera tus peores momentos. El p99 es el amigo que lo recuerda todo.
Un control de cordura y luego seguimos adelante. El generador de perfiles también extrae los contadores de utilización de GPU DCGM cada 100 ms y los une a cada iteración. En la ventana compartida, la actividad SM y DRAM del trabajador FFT aumenta drásticamente (sus ciclos de ejecución ahora se superponen con un GEMM que golpea el mismo sistema de memoria); en la ventana individual, no lo hacen. Entonces, la disputa aparece en dos capas completamente independientes (latencia de la aplicación y contadores de hardware), que es la forma en que sabes que esto es real y no un artefacto de cronómetro.
6. Se trata de enjambres de agentes, no de una sola carga de trabajo.
Se podría etiquetar fácilmente la sección 5 como “una FFT y un matmul peleados por una GPU, lo que no sorprende en absoluto a nadie que haya escrito alguna vez un kernel CUDA”, pero eso pierde el sentido por completo. Los dos trabajadores son simplemente sustitutos convenientes y mensurables de un patrón que aparece en el instante en que colocas un enjambre de agentes real en un hardware compartido:
Los agentes ligeros y sujetos a plazos: enrutadores, barandillas, clasificadores, llamadores de herramientas, modelos pequeños y rápidos. Baratos individualmente, en constante funcionamiento y todo el proceso está pendiente de ellos. (El trabajador de FFT es un ejemplo concreto de esta personalidad). Los agentes pesados y estables: los grandes pases directos del transformador, las llamadas al modelo vinculado a GEMM que dominan la computación. (El trabajador de GEMM es un ejemplo concreto de ello).
Coloque dos agentes con esas formas en una GPU con división de tiempo y obtendrá exactamente lo que medí: las medianas apenas se contraen, pero el agente pequeño y de latencia crítica se come la cola. No importa lo que hagan los agentes; importa cómo se comportan en los SM: uno necesita terminar rápido y con frecuencia, el otro solo quiere trabajar. Las manos que cortan el tiempo se turnan. No da plazos. Entonces, el agente que vive o muere cuando llega su fecha límite es el que sufre cuando su turno sigue siendo interrumpido.
Ese es el hilo conductor de toda esta serie. La parte 1 trataba de no repetir el trabajo entre agentes (compartir el caché KV). Esta parte se trata de no mentirte a ti mismo sobre lo que les cuesta a esos agentes compartir la GPU. La división del tiempo le permite adquirir capacidad (más ranuras programables en una tarjeta) y no le proporciona ningún aislamiento. Mire solo los promedios y su agente más sensible a los plazos se interrumpirá primero, silenciosamente, en el p99, mientras cada módulo sigue parpadeando en Ejecución.
7. “Entonces… ¿cómo lo ejecuto?”
El oleoducto es deliberadamente aburrido, porque en la ingeniería de sistemas, "emocionante" suele significar que la producción está en marcha. Es un gráfico lineal de compilación → clúster → registros → métricas impulsado desde la raíz del repositorio:
run.py crea la imagen del trabajador con Podman, la importa al contenedor de K3, crea el espacio de nombres, opcionalmente inicia un hilo de raspado DCGM, aplica los trabajos, espera y recopila registros en logs/run-/. Los trabajadores emiten esas líneas KTS_APP por iteración que viste arriba. generate_results.py analiza los registros, recorta el calentamiento, calcula p50/p95/p99, el proxy de rendimiento, el factor de degradación y la unión DCGM, luego escribe data/summary.{csv,json}, los gráficos y un docs/results.md.
En un nodo que ya tiene K3, el controlador NVIDIA, el Container Toolkit, el complemento del dispositivo y nvidia RuntimeClass, todo son tres comandos:
# 1. Instale ConfigMap de división de tiempo y vuelva a cargar el complemento del dispositivo kubectl apply -f time-slicing-config.yaml # 2. Cree la imagen del trabajador y ejecute el punto de referencia completo (compilación, importación, trabajos, registros) python3 run.py # 3. Convierta los registros en resúmenes, gráficos y una página de resultados python3 generate_results.py
¿El enlace del repositorio? Bueno, puedes encontrarlo cerca de la parte superior del artículo. Y felicidades por haber llegado hasta aquí. ¡Casi no pensé que alguien lo lograría!
8. Advertencias honestas (porque los comentarios van llegando)
Este es un estudio pequeño y deliberado, no un modelo de capacidad de un centro de datos. Esto es exactamente lo que no es, antes de que alguien me lo publique:
Son dos agentes, no cincuenta. La configuración expone cuatro ranuras lógicas; la ejecución resaltada empareja un trabajador FFT con un trabajador GEMM. Ese es el caso de controversia interesante más pequeño, elegido para mayor claridad. Llenar los cuatro espacios (la matriz de contención completa) está en la hoja de ruta, no en estos números. No estoy informando resultados de cincuenta agentes porque no medí cincuenta agentes. El rendimiento es un proxy de tasa media. 1000/latencia media es una tasa de iteración, no un rendimiento de servicio de solicitudes en un proceso de llegada real. Se gana la vida por el punto de "los promedios ocultan la cola" y nada más sofisticado. Las cargas de trabajo son sintéticas. Una FFT en bucle y un matmul en bucle son sustitutos honestos de un agente ligero sensible a la latencia y un agente de inferencia intensa, pero no son un modelo completo detrás del tráfico real. La forma de interferencia se generaliza; los milisegundos absolutos no lo hacen. La actividad del DCGM es un indicador de baja magnitud. Los trabajadores se adaptan al ritmo del sueño, por lo que la GPU está inactiva mucho y los medios SM/DRAM parecen pequeños. Trátelos como señales relativas, dentro del estudio: corroboran la historia de la latencia, no afirman una saturación total. La división del tiempo no es el único modo de compartir. Como establece el §7, este estudio mide deliberadamente la ruta predeterminada: la que la mayoría de las personas obtienen en el momento en que activan el uso compartido de GPU. Un enfrentamiento directo con MPS y MIG es una publicación separada. Una clase de GPU, una ejecución resaltada. Los números provienen de una sola Pascal GTX 1080. Las GPU más nuevas cambian de contexto más rápido y las colas absolutas se reducen; la dirección (el agente pequeño sensible a la latencia se degrada primero) es el resultado duradero.
Nada de esto mueve la comida para llevar. Simplemente me mantiene honesto sobre su alcance, y en el momento en que una publicación de referencia oculta sus advertencias, es el momento en que sus números dejan de valer algo.
9. Wrap (y la configuración para la Parte 3)
La división del tiempo de Kubernetes es una ilusión maravillosa. Le dice a su programador que una GPU es cuatro, permite que cuatro pods informen en ejecución y luego los encierra silenciosamente en una habitación para pelear por el bus de memoria. Para trabajos con un rendimiento limitado y plazos relajados, esa ilusión es inofensiva y genuinamente útil. Para los miembros sensibles a la latencia de un enjambre de agentes, la ilusión se esconde exactamente donde no están mirando: el p99.
La solución no es prohibir el uso compartido de GPU: hay que compartir hardware, a menos que se tenga un presupuesto infinito. La solución es dejar de utilizar una marca de verificación verde YAML como sustituto de la realidad microarquitectónica. Mida la cola, atribuya la degradación y programe teniendo en cuenta los límites reales del silicio. Kube-TimeSlice-Profiler es un paso en la dirección correcta: convierte la vaga sensación de que "la GPU parece lenta hoy" en un factor de degradación medible con recibos.
Si llegó aquí como un principiante que solo quería saber por qué "ambos pods se están ejecutando" no significa "ambos agentes están contentos": felicitaciones, ahora comprende el uso compartido de GPU mejor que la marca de verificación verde. Anímate y desconfía de tus promedios, ¡estás listo!
Próximamente: El Paseo de la Vergüenza de PCIe
Acabamos de sobrevivir a dos agentes peleando por una sola GPU sin mentirnos sobre la cola de latencia. Pero hay otro impuesto silencioso escondido en cada tubería de RAG: el viaje PCIe.
En este momento, cada vez que un agente necesita recuperar contexto, hace una pausa, abandona el acelerador, se arrastra por el bus PCIe de regreso a Python, ejecuta una búsqueda vectorial en la CPU y regresa penosamente.
En la Parte 3, acabaremos con ese viaje. Construiremos un kernel CUDA Top-K personalizado para mantener todo el bucle de recuperación atrapado en el hardware de la GPU, sin ida y vuelta de Python ni retrasos en el lado del host. GPU del mismo presupuesto. La misma filosofía de “dejar de desperdiciar hardware”.
Nos vemos en la Parte 3.
Descargo de responsabilidad: las ilustraciones de este artículo se generaron utilizando IA (Claude Opus 4.8). Son ilustrativos, no fotográficos, y cualquier etiqueta visible dentro de las imágenes está estilizada en lugar de autorizada; consulte el cuerpo del artículo y el código mismo para obtener nombres precisos de funciones, valores métricos y detalles de arquitectura.