Ejecución Paralela
Avanzado⏱35 min📖3 min de lecturaLección 8 de 10

Ejecución Paralela

Domina la ejecución paralela de nodos en LangGraph, patrones de fan-out/fan-in, coordinación de ramas, seguridad de hilos y sincronización de resultados paralelos.

LangGraph admite la ejecución paralela de nodos mediante aristas de fan-out. Esta lección cubre cuándo y cómo paralelizar, coordinar ramas paralelas, fusionar resultados y tratar la seguridad de los hilos.


Cómo Funciona la Ejecución Paralela

Cuando un nodo tiene múltiples aristas de salida, todos los nodos de destino se ejecutan simultáneamente en hilos separados:

python
# Fan-out: B and C run in parallel after A builder.add_edge("A", "B") builder.add_edge("A", "C")
100%

Cada rama recibe una copia del estado en el punto de fan-out. Cuando todas las ramas terminan, sus actualizaciones se fusionan antes de que se ejecute el nodo de fan-in.


Fan-Out/Fan-In Básico

python
from langgraph.graph import StateGraph, START, END from typing_extensions import TypedDict, Annotated from typing import List from operator import add class ParallelState(TypedDict): input_text: str results: Annotated[List[str], add] merged: str def splitter(state: ParallelState) -> dict: """Fan-out: this node passes state to multiple parallel nodes.""" return {} # Passes state through unchanged def process_a(state: ParallelState) -> dict: return {"results": [f"Process A: {state['input_text'].upper()}"]} def process_b(state: ParallelState) -> dict: return {"results": [f"Process B: {state['input_text'].lower()}"]} def process_c(state: ParallelState) -> dict: return {"results": [f"Process C: {state['input_text'][::-1]}"]} def merger(state: ParallelState) -> dict: combined = "\n".join(state["results"]) return {"merged": combined} builder = StateGraph(ParallelState) builder.add_node("splitter", splitter) builder.add_node("process_a", process_a) builder.add_node("process_b", process_b) builder.add_node("process_c", process_c) builder.add_node("merger", merger) builder.add_edge(START, "splitter") builder.add_edge("splitter", "process_a") builder.add_edge("splitter", "process_b") builder.add_edge("splitter", "process_c") builder.add_edge("process_a", "merger") builder.add_edge("process_b", "merger") builder.add_edge("process_c", "merger") builder.add_edge("merger", END) app = builder.compile()
ℹ️Nota

Los tres nodos de procesamiento (A, B, C) se ejecutan en paralelo. El nodo de fusión espera a que los tres terminen antes de ejecutarse.


Paralelismo Dinámico

Haz fan-out dinámicamente según el número de elementos a procesar:

python
def dispatch(state: State) -> dict: items = state["items"] # Dynamic dispatch creates parallel work return {"item_count": len(items)} # The splitter node and multiple parallel workers # process different chunks simultaneously

Para un fan-out verdaderamente dinámico, usa un patrón map:

python
def map_node(state: State) -> dict: items = state["items_to_process"] results = [] # Process each item (can parallelize by fan-out) for item in items: result = process_item(item) results.append(result) return {"all_results": results}

Seguridad de Hilos

Las ramas paralelas comparten el estado, pero se ejecutan en hilos separados. Cada rama recibe una instantánea del estado en el punto de fan-out:

python
import threading import time def parallel_worker(state: State) -> dict: thread_id = threading.get_ident() print(f"Thread {thread_id} processing: {state.get('shared_value', 'N/A')}") # Simulate work time.sleep(1) return {"worker_result": f"Thread {thread_id} done"} # Each worker sees the same state snapshot # Their updates are merged when all complete
📌Importante

Las ramas paralelas no deben depender de los resultados de las demás. Cada rama trabaja a partir de la misma instantánea de estado. Usa nodos de fan-in para coordinar una vez terminado el trabajo paralelo.


Patrones de Reducción (Fan-In)

Agregación Simple

python
def reducer(state: State) -> dict: all_results = "\n".join(state.get("results", [])) return {"final": all_results}

Fusión Ponderada

python
def weighted_merger(state: State) -> dict: results = state.get("results", []) weights = state.get("weights", {}) final = "" for result in results: source = result.split(":")[0] weight = weights.get(source, 1.0) if weight > 0.5: final += f"[High confidence] {result}\n" return {"merged": final}

Fusión por Votación

python
def voting_merger(state: State) -> dict: results = state.get("results", []) from collections import Counter answers = Counter(results) most_common = answers.most_common(1)[0] return {"consensus": most_common[0], "votes": most_common[1]}

Ejecución Paralela de Herramientas

Ejecuta varias herramientas en paralelo:

python
from langgraph.prebuilt import ToolExecutor @tool def search_web(q: str) -> str: return f"Web: {q}" @tool def search_db(q: str) -> str: return f"DB: {q}" @tool def search_api(q: str) -> str: return f"API: {q}" tools = [search_web, search_db, search_api] tool_executor = ToolExecutor(tools) def parallel_search(state: State) -> dict: query = state["query"] # Dispatch all three searches in parallel via fan-out return { "web_query": query, "db_query": query, "api_query": query } # Each search node runs in parallel def web_search_node(state: State) -> dict: result = tool_executor.invoke({"name": "search_web", "args": {"q": state["web_query"]}}) return {"web_result": str(result)} # Same for db_search_node and api_search_node... def aggregate_results(state: State) -> dict: combined = f"Web: {state['web_result']}\nDB: {state['db_result']}\nAPI: {state['api_result']}" return {"final": combined}

Llamadas LLM Paralelas

Usa fan-out paralelo para obtener múltiples perspectivas de LLM:

python
def perspective_a(state: State) -> dict: response = llm.invoke(f"Analyze {state['topic']} from an economic perspective") return {"perspectives": [f"Economic: {response.content}"]} def perspective_b(state: State) -> dict: response = llm.invoke(f"Analyze {state['topic']} from a social perspective") return {"perspectives": [f"Social: {response.content}"]} def perspective_c(state: State) -> dict: response = llm.invoke(f"Analyze {state['topic']} from a technical perspective") return {"perspectives": [f"Technical: {response.content}"]} # All three run in parallel → merger synthesizes
💡Consejo

Las llamadas paralelas a LLM con distintos prompts de sistema te permiten reunir perspectivas diversas simultáneamente y luego sintetizarlas en una respuesta integral.


Manejo de Errores en Ramas Paralelas

python
def safe_parallel_worker(state: State) -> dict: try: result = risky_operation(state["input"]) return {"results": [f"Success: {result}"]} except Exception as e: return {"errors": [f"Worker failed: {e}"]} def error_aware_merger(state: State) -> dict: results = state.get("results", []) errors = state.get("errors", []) if errors and not results: return {"merged": f"All workers failed: {errors}"} if errors: return {"merged": f"Partial results: {results}\nErrors: {errors}"} return {"merged": "\n".join(results)}
⚠️Advertencia

Una excepción en una rama paralela no cancela las demás ramas. Todas las ramas terminan; sus resultados se fusionan, incluidos los campos de error.


Consideraciones de Rendimiento

Factor
Impacto
Mitigation
Sobrecarga de hilos
Iniciar hilos tiene costo
Paraleliza solo trabajo > 100ms
Bloqueo de estado compartido
Las escrituras requieren sincronización
Usa el reducer add para las escrituras
Amplitud del fan-out
Demasiadas ramas = contención
Limita a 5-10 ramas paralelas
Memoria
Cada rama tiene su propio contexto
Mantén el estado pequeño
Límites de tasa del LLM
Las llamadas paralelas alcanzan los límites más rápido
Añade limitación de tasa

Ejemplo Completo de Procesamiento Paralelo

python
from langgraph.graph import StateGraph, START, END from typing_extensions import TypedDict, Annotated from typing import List from operator import add class DataState(TypedDict): data: str chunks: List[str] chunk_results: Annotated[List[str], add] final_output: str def split_data(state: DataState) -> dict: # Split large text into chunks for parallel processing chunk_size = len(state["data"]) // 3 chunks = [ state["data"][:chunk_size], state["data"][chunk_size:2*chunk_size], state["data"][2*chunk_size:] ] return {"chunks": chunks} def process_chunk_1(state: DataState) -> dict: return {"chunk_results": [f"Chunk 1: {state['chunks'][0][:50]}..."]} def process_chunk_2(state: DataState) -> dict: return {"chunk_results": [f"Chunk 2: {state['chunks'][1][:50]}..."]} def process_chunk_3(state: DataState) -> dict: return {"chunk_results": [f"Chunk 3: {state['chunks'][2][:50]}..."]} def merge_results(state: DataState) -> dict: return {"final_output": "\n".join(state["chunk_results"])} builder = StateGraph(DataState) builder.add_node("split", split_data) builder.add_node("chunk1", process_chunk_1) builder.add_node("chunk2", process_chunk_2) builder.add_node("chunk3", process_chunk_3) builder.add_node("merge", merge_results) builder.add_edge(START, "split") builder.add_edge("split", "chunk1") builder.add_edge("split", "chunk2") builder.add_edge("split", "chunk3") builder.add_edge("chunk1", "merge") builder.add_edge("chunk2", "merge") builder.add_edge("chunk3", "merge") builder.add_edge("merge", END) app = builder.compile()

Preguntas de Práctica (Practice Questions)

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

How does LangGraph execute nodes with multiple outgoing edges?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What does a fan-in node receive?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What state snapshot do parallel branches see?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What happens if one parallel branch raises an exception?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What is the recommended maximum number of parallel branches?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What reducer is ideal for collecting results from parallel branches?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

How can you parallelize LLM calls with different perspectives?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

When is parallel execution NOT beneficial?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

What pattern processes a large dataset by splitting, parallel processing, and merging?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

How does the 'add' reducer help with parallel execution?


✅Éxito

Conclusiones Clave

  • Las aristas de fan-out crean ramas de ejecución paralela
  • Cada rama ve la misma instantánea de estado en el fan-out
  • Los nodos de fan-in esperan a que todas las ramas terminen antes de ejecutar
  • Annotated[list, add] acumula resultados paralelos de forma limpia
  • Maneja errores por rama usando try/except
  • 5-10 ramas paralelas es un buen máximo para la mayoría de los sistemas
  • Las llamadas paralelas a LLM con distintos prompts reúnen perspectivas diversas
  • Patrón Map-Reduce: dividir → procesar en paralelo → fusionar
  • Las tareas cortas (< 100ms) pueden no beneficiarse de la paralelización
Progreso80%