RequestQueue: FCFS y cola por prioridad
Responsabilidades
El Scheduler mantiene internamente tres colas: waiting, skipped_waiting y running (waiting/running:181-184). Las dos primeras son instancias de RequestQueue, encargadas de poner en cola los requests antes de que reciban bloques KV. RequestQueue (RequestQueue:20) es una clase base abstracta que define un conjunto común de interfaces (add_request, pop_request, peek_request, prepend_request, remove_request, etc.); el orden concreto lo deciden dos implementaciones: FCFSRequestQueue (FCFSRequestQueue:75) y PriorityRequestQueue (PriorityRequestQueue:131).
El motivo de ser de la cola es ofrecer al planificador un punto único de "dame el siguiente request a planificar": da igual que por debajo haya un deque o un heap, el bucle principal del planificador solo llama peek_request() / pop_request() (peek_request:650). La política de encolado la decide scheduler_config.policy, y se inyecta en Scheduler.__init__ vía create_request_queue(self.policy) (create_request_queue:181).
Motivación de diseño
¿Por qué abstraer la cola en vez de usar directamente list[Request]?
- Política intercambiable: el enum
SchedulingPolicy(SchedulingPolicy:13) solo listaFCFSyPRIORITY, pero la clase base esconde los detalles de ordenación en la implementación. El planificador habla solo con la interfazRequestQueue; añadir una nueva política (p. ej. fair scheduling) basta con escribir una nueva subclase. - FCFS va con deque:
FCFSRequestQueue(deque[Request], RequestQueue)(FCFSRequestQueue:75) reutiliza directamentecollections.dequede Python; append/popleft en O(1) por ambos extremos es justo la implementación óptima para FCFS.add_requestesappend, y pop espopleft(add/pop:78-84). - Priority va con heap:
PriorityRequestQueueusaheapq(heappush:144); el orden lo decide el__lt__del propioRequest: primero porpriorityascendente, luego porarrival_timeascendente (docstring:131-139), permitiendo asignar a cada request una prioridad. - Semántica unificada de prepend: tras una preemption o cuando se desbloquea una dependencia asíncrona, hace falta devolver un request a la cabeza de la cola para que se reprograne primero. Ambas colas ofrecen
prepend_request, pero en la cola priority no existe el concepto de "cabeza", así queprepend_requestdegenera enadd_request(PriorityRequestQueue.prepend:160-165), aclarado en el docstring. skipped_waitingreutiliza el mismo tipo: los requests saltados por exceder LoRA, por async KV load, etc., se meten enskipped_waiting(step_skipped_waiting:638); es la misma clase de cola quewaiting, y el bucle principal del planificador usa_select_waiting_queue_for_scheduling()para elegir entre las dos la que tenga la cabeza más temprana (_select_waiting_queue_for_scheduling:1867-1877).
Archivos clave
SchedulingPolicy:13—Enum, soloFCFSyPRIORITY.RequestQueue ABC:20— clase base abstracta que defineadd_request/pop_request/peek_request/prepend_request/remove_request/__bool__/__len__/__iter__.FCFSRequestQueue:75— hereda dedeque[Request]+RequestQueue; todas las operaciones son llamadas nativas del deque.FCFS add/pop:78-84—add_requestva porappend,pop_requestporpopleft; la implementación estándar de FCFS.FCFS prepend:92-94—prepend_requestusaappendleft, de modo que un request devuelto tras preemption se reprograne primero en el siguiente paso.FCFS prepend_requests:96-103—prepend_requestsusaextendleft; ojo al docstring que recuerda que el orden de los prepended queda invertido respecto al original.FCFS remove_requests:109-116— como el deque no soporta filter in-place,remove_requestshaceclear()+extend().PriorityRequestQueue:131— mantiene_heap: list[Request]conheapq.Priority add/pop:144-152—heappushpara encolar,heappoppara desencolar; el orden lo decideRequest.__lt__.Priority remove:175-184—remove_requestprimero hacelist.removey luegoheapify; O(n), pero semánticamente correcto.Priority __iter__:194-198— al iterar, copia el heap y luego hace heappop uno a uno, garantizando un recorrido por orden de prioridad sin romper el heap original.create_request_queue:201— función factoría que instancia la cola correspondiente segúnSchedulingPolicy.
Flujo de datos
Cuando el bucle principal del planificador necesita, en cada step, elegir el siguiente request desde la cola waiting para planificar, llama peek_request():
request_queue = self._select_waiting_queue_for_scheduling()
assert request_queue is not None
request = request_queue.peek_request()
request_id = request.request_id
# try to promote blocked statuses while traversing skipped queue.
if self._is_blocked_waiting_status(
request.status
) and not self._try_promote_blocked_waiting_request(request):
if request.status == RequestStatus.WAITING_FOR_REMOTE_KV:
logger.debug(
"%s is still in WAITING_FOR_REMOTE_KV state.",
request_id,
)
request_queue.pop_request()
step_skipped_waiting.prepend_request(request)
continueEsto está en scheduler.py:647-664. Ojo: aquí solo se hace peek_request sin hacer pop inmediato; solo cuando el request pasa de verdad por toda la cadena de comprobaciones (prefix cache, presupuesto de encoder, límites de LoRA, etc.) se llama request_queue.pop_request() en pop_request:946 para sacarlo y promoverlo a running. Si cualquier comprobación intermedia falla, se usa pop_request() + step_skipped_waiting.prepend_request() para moverlo a la cola skipped sin bloquear a los que vienen detrás.
_select_waiting_queue_for_scheduling (_select_waiting_queue_for_scheduling:1867) decide de cuál de las dos colas (waiting o skipped_waiting) se saca en esta ronda. En modo FCFS es simple: skipped_waiting or waiting or None, priorizando los requests que vuelven de skipped. En modo priority compara las dos cabezas por (priority, arrival_time) y elige la menor.
La entrada por la cola se hace en Scheduler.add_request (add_request:2012), que llama a _enqueue_waiting_request (_enqueue_waiting_request:1861) y luego, según el estado del request, lo manda a waiting o a skipped_waiting. El ciclo de vida completo: entra en waiting → asciende a running → es preemptado y vuelve vía waiting.prepend_request → vuelve a ascender a running; toda manipulación de colas pasa por la interfaz RequestQueue.
Límites y fallos
peek_requestlanza IndexError en cola vacía:FCFSRequestQueue.peek_requestcuando está vacía haceraise IndexError("peek from an empty queue")(FCFS peek:88-90);PriorityRequestQueueigual (Priority peek:156-158). El planificador usa_select_waiting_queue_for_schedulingpara garantizar que nunca hace peek sobre una cola vacía, pero la implementación no se defiende por sí misma.remove_requestes O(n): FCFS pasa pordeque.remove(O(n)), priority porlist.remove+heapify(O(n) + O(n) de reordenación) (Priority remove:175-178); para colas largas hay coste, pero la semántica es correcta.remove_requestsno deduplica: la implementación FCFS hacefiltered_requests = [req for req in self if req not in requests_to_remove](FCFS remove_requests:109-116);requests_to_removedebe ser un set para que la comprobación sea O(1); si el llamador pasa una lista, se degrada a O(n*m).prepend_requestsinvierte el orden: FCFS usaextendleft(el docstring avisa de que el orden de los prepended queda invertido respecto al orden de aparición en la cola original) (FCFS prepend_requests:96-103); la cola priority degenera en llamadas sueltas aadd_request, donde el orden no importa (Priority prepend_requests:167-173).__iter__de la cola priority copia:heap_copy = self._heap[:]y luego heappop uno a uno (Priority __iter__:194-198) para garantizar un recorrido por orden de prioridad sin romper el heap original, a cambio de una copia O(n) en memoria.- Política desconocida lanza ValueError:
create_request_queue(create_request_queue:201-208) haceraise ValueErrorparaSchedulingPolicydesconocido; el constructor de Scheduler además lo envuelve en un try/except que relanza un error más amable (policy 校验:174-179).
Resumen
RequestQueue es la abstracción de sala de espera del planificador: FCFSRequestQueue va con deque, PriorityRequestQueue va con heap, con interfaz unificada. El bucle principal de Scheduler.schedule() solo habla con esta interfaz; la política de encolado la decide scheduler_config.policy. Para ver cómo schedule usa estas dos colas, leer Scheduler.schedule; para ver cómo se devuelve un request a la cabeza tras preemption, leer Preempt y prefill throttle.
Véase la documentación oficial: Documentación de vLLM · README.