Transformers documentation

Loteo continuo

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Loteo continuo

El loteo continuo maximiza la utilización de la GPU al reprogramar dinámicamente el lote en cada paso de generación. A medida que las solicitudes terminan, las nuevas se unen de inmediato en lugar de esperar a que todo el lote se complete. La GPU se mantiene al máximo y el rendimiento se mantiene alto.

Para implementaciones en producción, use transformers serve. Se basa en ContinuousBatchingManager y expone un punto de finalización HTTP compatible con OpenAI.

generate_batch

El loteo continuo se admite a través de generate_batch(). Pasa una lista de prompts tokenizados y obtén los resultados de todos cuando terminen. generate_batch gestiona la programación internamente y bloquea hasta que todas las solicitudes estén completas.

Para casos de uso de servicio y transmisión, use ContinuousBatchingManager directamente para gestionar las solicitudes.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.generation import ContinuousBatchingConfig, GenerationConfig

model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-4B",
    attn_implementation="flash_attention_2",
    device_map="auto",
    dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B")

prompts = [
    "What's up?",
    "Name a cat breed.",
    "Write a detailed history of quantum mechanics.",
]
inputs = [tokenizer.encode(p) for p in prompts]

generation_config = GenerationConfig(
    max_new_tokens=64,
    eos_token_id=tokenizer.eos_token_id,
)

outputs = model.generate_batch(inputs=inputs, generation_config=generation_config)

for request_id, output in outputs.items():
    text = tokenizer.decode(output.generated_tokens, skip_special_tokens=True)
    print(f"[{request_id}] {text}")

ContinuousBatchingManager

ContinuousBatchingManager ejecuta un hilo en segundo plano y permite enviar solicitudes y obtener resultados de forma independiente. En cada paso de generación, comprueba las solicitudes finalizadas y programa nuevas para unirse al lote. Esto resulta útil para el streaming, el servicio en tiempo real o el envío de solicitudes a medida que llegan.

Use continuous_batching_context_manager() para iniciar y detener el gestor de forma segura. El ejemplo siguiente contiene entradas de longitud variable. En cuanto el prompt más corto se completa, sale del lote mientras los prompts más largos continúan generando. Con el loteo estático, habría que rellenarlos todos hasta la misma longitud. El loteo continuo libera el prompt completado para que puedas comenzar a procesar el siguiente prompt de inmediato.

with model.continuous_batching_context_manager(generation_config=generation_config) as manager:
    manager.add_request(
        input_ids=tokenizer.encode("Write a detailed history of quantum mechanics."),
        request_id="long",
        max_new_tokens=512,
    )
    manager.add_request(
        input_ids=tokenizer.encode("What's up?"),
        request_id="short_0",
        max_new_tokens=32,
    )
    manager.add_request(
        input_ids=tokenizer.encode("Name a cat breed."),
        request_id="short_1",
        max_new_tokens=32,
    )

    for result in manager:
        text = tokenizer.decode(result.generated_tokens, skip_special_tokens=True)
        print(f"[{result.request_id}] {text}")

También podrías llamar a init_continuous_batching() para gestionar el ciclo de vida por tu cuenta.

manager = model.init_continuous_batching(generation_config=generation_config)
manager.start()

# submit and retrieve requests...

Apagar el administrador

El gestor ejecuta un hilo en segundo plano y mantiene los recursos distribuidos. El cierre se realiza en dos etapas para que puedas elegir qué hacer con el trabajo en curso.

Llame a stop() para detener el hilo en segundo plano. De forma predeterminada, el gestor deja de aceptar nuevas solicitudes y espera a que finalicen las solicitudes en cola y las activas antes de que el hilo termine.

manager.stop()

Pase hard_stop=True para abandonar el trabajo pendiente de inmediato. Las solicitudes en cola y activas fallarán con un RuntimeError en lugar de completarse.

manager.stop(hard_stop=True)

Una vez que se llama a stop, add_request() y add_requests() descartan los nuevos envíos y registran una advertencia. Aún puedes llamar a start de nuevo para ejecutar otra sesión de generación con el mismo administrador.

Llame a destroy() para liberar los recursos distribuidos. destroy detiene primero el manager si aún está en ejecución, y el manager no puede reiniciarse después. Úselo cuando haya terminado con el continuous batching para la vida útil del proceso.

manager.destroy()

continuous_batching_context_manager() gestiona este proceso. Llama a stop al salir y a destroy a menos que pases persistent_manager=True para cachear el gestor en el modelo para la siguiente sesión.

Agregar solicitudes

add_request() envía una sola solicitud. Proporcione un request_id o deje que el generador lo cree automáticamente.

manager.add_request(input_ids=input_ids, request_id="my_request")

add_requests() envía un lote a la vez. Ordena las entradas automáticamente para maximizar los aciertos de la caché de prefijo cuando el intercambio de bloques está habilitado.

manager.add_requests(inputs=inputs)

Cancela una solicitud con cancel_request().

manager.cancel_request(request_id="my_request")

Parámetros de muestreo por solicitud

Active per_request_processors para aplicar temperature, top_k y top_p de forma independiente por solicitud dentro de la misma pasada hacia adelante, lo que permite usar parámetros de muestreo diferentes para distintas solicitudes (por ejemplo, salidas creativas con temperatura alta frente a salidas precisas con temperatura baja).

cb_config = ContinuousBatchingConfig(per_request_processors=True)

# each request gets its own sampling parameters
manager.add_request(input_ids=inputs_a, temperature=0.9, top_p=0.95)
manager.add_request(input_ids=inputs_b, temperature=0.1, top_k=10)

Cada parámetro en GenerationConfig debe tener un valor no predeterminado para crear el procesador de logits asociado en tiempo de ejecución. Por ejemplo, establezca temperature en un valor distinto de None o 1 para admitir el control de temperatura por solicitud. Las solicitudes con temperaturas de 1 aún pueden crearse después.

Obtención de resultados

Itera sobre el manager para recibir los resultados a medida que llegan.

for result in manager:
    print(tokenizer.decode(result.generated_tokens, skip_special_tokens=True))

get_result() obtiene el siguiente resultado de la cola de salida. Pasa un request_id para filtrar una solicitud específica. Si el siguiente resultado de la cola no coincide, se vuelve a encolar y el método devuelve None.

# next available result
result = manager.get_result()

# filter for a specific request
result = manager.get_result(request_id="my_request")

Streaming

Establezca streaming=True en una solicitud y, a continuación, use request_id_iter() para iterar sobre las salidas parciales a medida que se generan los tokens.

from transformers.generation.continuous_batching import RequestStatus

manager.add_request(input_ids=input_ids, request_id="streamed", streaming=True)

for chunk in manager.request_id_iter(request_id="streamed"):
    token = tokenizer.decode(chunk.generated_tokens[-1:], skip_special_tokens=True)
    print(token, end="", flush=True)
    if chunk.status == RequestStatus.FINISHED:
        break

ContinuousBatchingConfig

ContinuousBatchingConfig controla la caché KV, la programación, los gráficos CUDA, el uso de memoria y más. Pásalo a generate_batch o a init_continuous_batching() con el argumento continuous_batching_config, de forma independiente de GenerationConfig. Las dos configuraciones describen cosas diferentes. GenerationConfig está centrada en el modelo y contiene los parámetros de muestreo y de parada, mientras que ContinuousBatchingConfig está centrada en el hardware y contiene los parámetros de memoria y de programación.

Establecer continuous_batching_config en un GenerationConfig está en desuso, emite una FutureWarning y se eliminará en la v5.19.

Por defecto, max_batch_tokens es 8192, limitado por la memoria de GPU disponible y nunca inferior a 256, mientras que num_blocks ocupa la memoria restante. Utilice la tabla siguiente para ayudarle a elegir las características adecuadas.

CaracterísticaMemoriaThroughputLatencia
max_memory_percent / page_size✓ controla el presupuesto de KV
max_batch_tokens↑ búferes de entrada más grandes✓ lotes de prefill más grandes✓ TTFT cuando el prefill es el cuello de botella
scheduler✓ política de programación✓ TTFT
Gráficos de CUDA↑ almacenamiento del grafo✓ menor sobrecarga de despacho✓
Loteo asíncrono↑ ~2× buffers de E/S✓ solapa CPU/GPU
Compilación↑ solo warmup-time✓ pases hacia adelante más rápidos✓
Ruta rápida de decodificación↑ tabla de bloques por solicitud✓ pasos de decodificación más rápidos✓
Descarga en CPU↑ memoria CPU fijada✓ omite algunas re-prellen
Caché de prefijos↓ bloques KV compartidos✓ omite el prefill redundante✓ TTFT
Atención paginada↓ sin fragmentación✓ membresía dinámica del lote
Ventana deslizante↓ KV acotado por capa
Procesadores por solicitud✓ parámetros de muestreo mixtos por lote
max_requests_per_batch↓ limita el búfer de logits✓ limita el tamaño del lote
safety_margin✓ protege la latencia de decodificación
from transformers.generation import ContinuousBatchingConfig

cb_config = ContinuousBatchingConfig(
    max_memory_percent=0.8,
    page_size=256,           # KV cache page size in tokens
    scheduler_type="fifo",        # "fifo" or "prefill_first"
)

outputs = model.generate_batch(
    inputs=inputs,
    generation_config=generation_config,
    continuous_batching_config=cb_config,
)

Tamaño de página de la caché KV

page_size define cuántos tokens de KV contiene una página de atención completa para una capa. Debe ser al menos 4, o la inicialización lanzará un ValueError. El valor predeterminado 256 funciona para la mayoría de las cargas de trabajo.

El parámetro block_size está obsoleto y ha sido reemplazado por page_size.

Un tamaño de página más grande reduce la gestión administrativa, pero desperdicia espacio cuando una secuencia se detiene a mitad de página. Las páginas más pequeñas desperdician menos espacio, pero requieren más gestión administrativa. Por lo general, no es necesario ajustar page_size. Si su carga de trabajo tiene muchas solicitudes cortas, pruebe con un valor más pequeño. Si tiene pocas solicitudes muy largas, pruebe con un valor más grande. Consulte Gestión de memoria para ver cómo se integran las páginas, los bloques y los fragmentos.

cb_config = ContinuousBatchingConfig(page_size=128)

Tamaño del lote de prellenado

max_batch_tokens establece el presupuesto de tokens para una sola pasada hacia adelante, es decir, el número máximo de tokens de consulta que el modelo procesa a la vez. Un presupuesto mayor permite agrupar más tokens de prompt en cada prefill, lo que aumenta el rendimiento del prefill y reduce el tiempo hasta el primer token en cargas de trabajo intensivas en prompts. El programador divide los prompts que superan el presupuesto en varios pasos. Consulte chunked prefill para ver cómo se manejan los prompts de tamaño excesivo.

Por defecto, max_batch_tokens es 8192. El valor está limitado por la memoria GPU disponible para que los tensores de entrada por lote no desplacen la caché KV, y nunca baja de 256. En una GPU con poca memoria libre, el límite lo reduce por debajo de 8192.

max_batch_tokens y num_blocks se asignan desde el mismo presupuesto de memoria, por lo que se compensan entre sí. Un presupuesto de tokens mayor asigna búferes de entrada más grandes y deja menos bloques de caché KV para solicitudes concurrentes o largas. Aumenta max_batch_tokens para mejorar el rendimiento de prefill cuando tienes más memoria disponible, y redúcelo para liberar memoria para más bloques KV o para evitar errores de memoria insuficiente.

cb_config = ContinuousBatchingConfig(max_batch_tokens=16384)

Límites de lote y programación

max_requests_per_batch limita cuántas solicitudes se ejecutan en una sola pasada hacia adelante. El modelo produce un tensor de logits dimensionado por el número de tokens del lote y el tamaño del vocabulario, y se convierte a fp32 para el muestreo, lo que duplica la huella de memoria. Cada solicitud solo necesita una predicción del siguiente token, por lo que un límite más pequeño mantiene este tensor más pequeño y evita errores de memoria insuficiente en lotes con mucha prellenado y vocabularios grandes. Por defecto, se establece en el número de prompts enviados, con un valor de respaldo de 1024, y luego se limita para que quepa en max_batch_tokens y num_blocks.

cb_config = ContinuousBatchingConfig(max_requests_per_batch=256)

safety_margin reserva una fracción de la capacidad libre de la caché para las solicitudes activas. Cuando la capacidad libre cae por debajo de safety_margin (una fracción del tamaño total de la caché), el programador deja de admitir nuevos prefills y solo continúa con las solicitudes de decodificación ya en curso. Esto prioriza finalizar el trabajo activo frente a iniciar nuevas solicitudes, lo que protege la latencia de decodificación y retrasa el offloading. El valor debe estar entre 0 y 1, donde 0 desactiva el margen.

cb_config = ContinuousBatchingConfig(safety_margin=0.15)

El valor predeterminado depende del programador. Para el programador FIFO, es 0.15 y para prefill_first, es 0.0. Consulte Admisión para ver cómo interactúa el margen con la programación.

Probabilidades logarítmicas

ContinuousBatchingConfig devuelve la probabilidad en logaritmo de cada token generado cuando return_logprobs=True. Esto es útil para el RL, donde los logprobs son una entrada para algunos de los bucles de entrenamiento.

cb_config = ContinuousBatchingConfig(return_logprobs=True)

# generate_batch()

for request_id, output in outputs.items():
    for token_id, log_prob in zip(output.generated_tokens, output.logprobs):
        token = tokenizer.decode([token_id])
        print(f"{token} | logprob: {log_prob}")

Gráficos de CUDA

Los gráficos de CUDA eliminan la sobrecarga de despacho de la CPU al registrar el gráfico de ejecución de la GPU una vez y reproducirlo para lotes con formas coincidentes. Actívalos explícitamente con use_cuda_graph=True.

cb_config = ContinuousBatchingConfig(use_cuda_graph=True)

Cuando está activo, el manager rellena las longitudes de la consulta y de KV hasta intervalos fijos para que las formas se repitan y los grafos se reutilicen. Valores más pequeños de q_padding_interval_size y kv_padding_interval_size reducen el cómputo desperdiciado en el relleno, pero esto implica que hay más formas únicas que el grafo debe registrar y almacenar, lo que consume más memoria.

cb_config = ContinuousBatchingConfig(
    use_cuda_graph=True,
    q_padding_interval_size=64,
    kv_padding_interval_size=16384,
)

Loteo asíncrono

El loteo asíncrono solapa la programación en CPU del siguiente lote con el cálculo en GPU del lote actual. Requiere gráficos de CUDA y duplica aproximadamente la VRAM utilizada para los tensores de entrada.

cb_config = ContinuousBatchingConfig(
    use_cuda_graph=True,
    use_async_batching=True,
)

Compilación

default_compile_level aplica torch.compile a las pasadas hacia adelante del modelo. La compilación intercambia un coste de calentamiento único por pasadas hacia adelante más rápidas durante la generación. Los niveles más altos ejecutan optimizaciones más agresivas, lo que mejora el rendimiento pero alarga el calentamiento. Mantenga el nivel bajo para pruebas e iteración de benchmarks, y aumente el nivel para cargas de trabajo de servicio de larga duración donde el coste de calentamiento se amortiza a lo largo de muchas solicitudes.

El nivel varía de 0 a 3. El nivel 0 es el predeterminado y omite la compilación por completo.

NivelmodedynamicCompensación
0——Sin compilación (predeterminado), inicio más rápido
1defaultTrueAceleración modesta, calentamiento breve
2max-autotune-no-cudagraphsTrueMás aceleración, calentamiento más largo
3max-autotune-no-cudagraphsFalseMejor rendimiento, mayor tiempo de calentamiento
cb_config = ContinuousBatchingConfig(default_compile_level=1)

El nivel proporciona una CompileConfig predeterminada para las rutas de ejecución varlen y decode. Solo se aplica a una ruta que no tenga una configuración explícita, por lo que varlen_compile_config y decode_compile_config tienen prioridad cuando están definidos. Bajo FlashAttention, la ruta varlen omite la compilación porque max_length_k provoca recompilaciones frecuentes, por lo que en ese caso el nivel solo afecta a la ruta decode.

Ruta rápida de decodificación

Cuando un lote contiene solo solicitudes de decodificación (un token de consulta por secuencia), el gestor puede despachar al kernel flash_attn_with_kvcache en lugar del kernel de longitud variable. Esto es más rápido que la ruta varlen porque el kernel lee y escribe la caché KV paginada in situ a través de una tabla de bloques, en lugar de pasar por una actualización manual. Consulte Paged attention para los detalles a nivel de kernel.

La ruta rápida se dimensiona mediante max_blocks_per_request, que dimensiona la tabla de bloques por solicitud. De forma predeterminada, este valor se infiere automáticamente. Si max_prompt_length y max_generated_length están configurados en el gestor, la tabla de bloques se dimensiona para acomodar la longitud máxima de la secuencia. En caso contrario, se utiliza un valor predeterminado de respaldo (32 bloques por solicitud).

Establezca max_blocks_per_request en un valor específico para dimensionar la tabla de bloques de forma explícita. Esto es útil cuando se conoce la longitud máxima de la secuencia por solicitud y se desea limitar el coste de memoria de la tabla de bloques.

cb_config = ContinuousBatchingConfig(max_blocks_per_request=64)

Establezca max_blocks_per_request=0 para deshabilitar la ruta rápida y forzar que cada lote pase por el kernel varlen. Esto recupera el comportamiento anterior al predeterminado y es útil cuando la ruta rápida no está disponible para su implementación de atención (el administrador también la deshabilita automáticamente cuando el kernel subyacente no se puede usar).

cb_config = ContinuousBatchingConfig(max_blocks_per_request=0)

La ruta rápida se basa en el kernel flash_attn_with_kvcache, que está disponible para dos combinaciones de dispositivo e implementación de atención.

Dispositivoattn_implementation
CUDAflash_attention_2 o flash_attention_3
XPUflash_attention_2

Para cualquier otra combinación, o cuando no se puede importar el kernel, el gestor recurre a la ruta varlen. Registra una advertencia solo cuando se establece max_blocks_per_request de forma explícita.

La atención completa admite tablas de bloques de kernel, pero la atención deslizante no. Si el modelo tiene alguna capa de atención deslizante, incluidos los modelos híbridos que también utilizan atención completa, la inicialización fuerza max_blocks_per_request a 0 y cualquier valor no nulo que se establezca se sobrescribe. A continuación, el gestor registra que la ruta rápida de decodificación no está disponible porque algunos de los tipos de atención del modelo no admiten tablas de bloques de kernel.

Descarga en CPU

El offloading de CPU copia los bloques de caché KV expulsados a un búfer de CPU fijo preasignado cuando la caché KV de la GPU está llena. Una vez que hay espacio disponible en la caché, el gestor copia los bloques de vuelta a la GPU y reanuda la solicitud sin recalcular su prompt ni los tokens generados.

Establezca cpu_offload_space en el espacio de intercambio de la CPU en GiB. El valor predeterminado, 0.0, desactiva la descarga en la CPU.

cb_config = ContinuousBatchingConfig(cpu_offload_space=8.0)

Por defecto, cpu_offload_space_safety_threshold=0.8 limita el espacio solicitado al 80% de la RAM del sistema disponible cuando psutil está instalado. Establezca cpu_offload_space=None para dimensionar el pool de intercambio a partir del umbral de seguridad.

Tiempo de espera de paralelismo de tensor

Bajo la paralelización de tensores, el gestor crea un grupo de comunicación de CPU para coordinar el envío, la cancelación y el cierre de solicitudes entre rangos. cpu_group_timeout limita el tiempo que una operación colectiva en este grupo puede bloquearse antes de que el proceso se detenga. Si un rango se detiene, el tiempo de espera evita que los demás esperen indefinidamente.

Establezca un tiempo de espera más largo para las cargas de trabajo que emiten colectivas poco frecuentes, o pase None para desactivarlo.

cb_config = ContinuousBatchingConfig(cpu_group_timeout=600.0)

Caché de prefijos

Cuando varias solicitudes comparten un prefijo común, como un prompt de sistema, el gestor reutiliza sus bloques de caché KV en lugar de recalcularlos. allow_block_sharing tiene un valor predeterminado de True. El uso compartido de prefijos solo está activo cuando todos los asignadores de tipos de atención admiten el uso compartido, por lo que solo los modelos de atención completa lo utilizan. Los modelos puros deslizantes e híbridos lo desactivan incluso cuando allow_block_sharing es True.

cb_config = ContinuousBatchingConfig(
    allow_block_sharing=True,  # default
)

Elegir un backend de atención

El loteo continuo (continuous batching) admite FlashAttention, SDPA y "paged|eager". Establece attn_implementation al cargar el modelo.

Backendattn_implementationRequisitos
FlashAttention 2“flash_attention_2”el paquete flash-attn o kernels
FlashAttention 3“flash_attention_3”el paquete flash-attn-3 o kernels
SDPA (nativo de PyTorch)“sdpa”Ninguno
Eager“paged|eager”Ninguno
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-4B",
    attn_implementation="flash_attention_2",
    device_map="auto",
    dtype=torch.bfloat16,
)

El loteo continuo (continuous batching) es más rápido con FlashAttention que con eager o SDPA, principalmente porque FlashAttention no necesita una máscara de atención. Si el modelo es compatible con FlashAttention, un modelo cargado con "eager" o "sdpa" cambia a FlashAttention 3, o a FlashAttention 2 si FlashAttention 3 no está instalado, y registra una advertencia. Si ninguna de las dos versiones está instalada, el backend no cambia.

Para mantener eager o SDPA, por ejemplo al realizar pruebas, establezca auto_switch_to_flash=False en ContinuousBatchingConfig. Siempre que el backend permanezca como "eager", el continuous batching ejecuta "paged|eager" en su lugar.

Paralelismo de tensores

Para modelos demasiado grandes para caber en una sola GPU, fragmenta los pesos entre dispositivos mediante paralelismo de tensores. Establece el número de dispositivos con DistributedConfig(tp_size=N). El loteo continuo lee el tamaño de paralelismo de tensores del modelo para dimensionar la caché KV paginada por fragmento. Consulta Paralelismo de tensores para ver la lista de arquitecturas compatibles y cómo funciona el fragmentado.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, DistributedConfig
from transformers.generation import ContinuousBatchingConfig, GenerationConfig

distributed_config = DistributedConfig(tp_size=4)
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-32B",
    attn_implementation="flash_attention_2",
    distributed_config=distributed_config,
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-32B")

inputs = [tokenizer.encode(p) for p in ["What's up?", "Name a cat breed."]]
generation_config = GenerationConfig(max_new_tokens=64, eos_token_id=tokenizer.eos_token_id)

outputs = model.generate_batch(inputs=inputs, generation_config=generation_config)

Ejecuta el script con torchrun, estableciendo --nproc-per-node en el número de GPUs en las que deseas distribuir el sharding.

torchrun --nproc-per-node 4 cb_tp.py

El tamaño de la paralelización de tensores debe dividir num_key_value_heads del modelo (consulte la configuración del modelo). De lo contrario, la caché paginada genera un error al iniciar, por lo que elija un --nproc-per-node adecuado.

No establezca device_map junto con distributed_config. Ambos entran en conflicto porque device_map coloca módulos completos en GPUs específicas, mientras que la paralelización de tensores fragmenta esos mismos parámetros entre todas las GPUs.

Atención con ventana deslizante

Los modelos con atención de ventana deslizante (Mistral, Gemma 2) funcionan con lotes continuos. Las capas deslizantes usan SlidingAttentionCacheAllocator, que mantiene solo una ventana fija de tokens recientes mediante el reciclaje de un pequeño conjunto de páginas. El tipo de atención proviene de layer_types o sliding_window del modelo, no de ContinuousBatchingConfig.

from transformers import AutoConfig, AutoModelForCausalLM

config = AutoConfig.from_pretrained("google/gemma-2-2b")
config.sliding_window = 4096

model = AutoModelForCausalLM.from_pretrained(
    "google/gemma-2-2b",
    config=config,
    attn_implementation="sdpa",
    device_map="auto",
    dtype=torch.bfloat16,
)

Prefix caching y la decode fast path se desactivan siempre que esté presente la atención deslizante, incluidos los modelos híbridos que también tienen capas de atención completa.

Próximos pasos

Update on GitHub