Skip to content

English · Español

02 — Contabilidad de coste

🇪🇸 El coste por petición tiene que ser una métrica de primera clase, no un Excel mensual. La fórmula es trivial — tiempo × tarifa — pero las decisiones difíciles (qué tarifa, cómo amortizar, cómo no contar dos veces el prefetch) deciden si el dashboard miente.

Un servicio web trata el coste como un problema financiero: la factura de cloud llega mensualmente, los de operaciones la reparten entre equipos. Servir LLM no puede tratar el coste así, porque el coste por petición varía 100× o más. Si no mides el coste por petición, no puedes:

  • detectar a un cliente abusando del servicio,
  • comparar dos versiones de modelo en un plano coste-calidad,
  • decidir si autoescalar,
  • ponerle precio a un producto.

Esta página deriva la fórmula, escoge la tarifa y lista las trampas.

La fórmula desde primeros principios

Una caja sirviendo tiene una tarifa \(r\) en dólares por segundo de tiempo de reloj de pared. Para una instancia cloud, \(r = (\text{hourly\_price}) / 3600\). Para el i5-8250U local de Borja con la tarifa nocional de $0.17/hr: \(r \approx 4.7 \times 10^{-5}\) USD/seg.

Una sola petición ocupa la caja durante un tiempo de reloj. Descompón en etapas:

\[T_{\text{req}} = T_{\text{retrieve}} + T_{\text{prefill}} + T_{\text{decode}}\]

(En el servidor con continuous-batching de la Fase 33, el tiempo de reloj de decode se comparte entre las peticiones activas; manejaremos esa corrección más abajo.)

Coste por petición ingenuo (sin batching):

\[\text{cost}_{\text{req}} = r \cdot T_{\text{req}}\]

Esa es la fórmula. La parte difícil son las correcciones.

Corrección 1: solapamiento por batching

Cuando el batcher está ejecutando N peticiones en paralelo, cada paso de decode procesa las N. El tiempo de reloj por paso de decode es el mismo que para una sola petición, pero el trabajo hecho es N×. El coste debe dividirse entre peticiones:

\[\text{cost}_{\text{decode}, i} = \frac{r \cdot T_{\text{decode-step}}}{N_{\text{active}}(\text{step})} \cdot n_{\text{steps}_i}\]

donde \(N_{\text{active}}(\text{step})\) es el número de peticiones compartiendo el batch en ese paso (que varía conforme las peticiones entran y salen).

Truco de implementación: el rastreador de coste no calcula esto exactamente por paso; eso es caro. En su lugar, el batcher reporta effective_decode_seconds_per_request = total_decode_wall_seconds / sum_over_steps(N_active). Cada petición añade effective × n_tokens_generated × r a su cuenta. El error agregado es ≤ 1% para perfiles de batch típicos.

Corrección 2: prefill cuadrático vs decode lineal

El cómputo de prefill escala como \(O(P^2)\) en la longitud del prompt \(P\) (attention). El cómputo de decode por paso escala como \(O(P + D)\) donde \(D\) son los tokens generados hasta ahora. Dos implicaciones:

  • Las peticiones con prompts largos son desproporcionadamente caras en prefill. Un prompt de 4 KB cuesta ~16× un prompt de 1 KB en prefill, no 4×.
  • Las peticiones con generación larga son aproximadamente lineales en coste. Doblar los tokens de salida dobla aproximadamente el coste de decode.

La fórmula de coste respeta esto automáticamente vía la medición del tiempo de reloj — no necesitas modelar \(P^2\) explícitamente, mides \(T_{\text{prefill}}\) y la cuadrática aparece en el número. Pero sí significa que el histograma de coste es algo bimodal: los prompts cortos se agrupan cerca de un suelo, los prompts largos tienen una cola pesada a la derecha.

Corrección 3: el retrieval es coste real

Si la petición dispara RAG (Fase 29), el tiempo de reloj de retrieval es parte del coste. Dos sub-piezas:

  • Embedding de la consulta. Decenas de milisegundos en CPU para un encoder pequeño; más largo si estás usando uno gordo.
  • Búsqueda vectorial. Para FAISS-flat sobre la KB de la Fase 29 (~miles de chunks), milisegundos de un solo dígito. Para HNSW, incluso menos.

El retrieval es pequeño (≤5% del coste total) para el setup de la Fase 29, pero debe rastrearse para que no se barra debajo de la alfombra — y para que si Borja más adelante cambia el modelo de embedding por uno más grande, el desplazamiento de coste sea visible.

Elegir la tarifa

Tres opciones honestas para la tarifa nocional $_per_hour:

Elección Valor Cuándo es la correcta
Equivalente cloud ~$0.17/hr (c5.xlarge) o $1.50/hr (g4dn.xlarge GPU) Comparando con un deployment cloud hipotético. Por defecto para el currículo.
Solo electricidad ~€0.01/hr para un i5-8250U a plena carga × €0.20/kWh Comparando el coste marginal de correr en hardware propio. Borja podría preferir esta para la caja local.
Local totalmente cargado Electricidad + hardware amortizado + alquiler + tiempo de admin La versión honesta para un producto real. Fuera de alcance para la Fase 34.

El laboratorio usa el equivalente cloud por defecto; Borja puede sobreescribir en la apertura de fase.

La codificación en Prometheus

Dos histogramas, buckets espaciados en log:

COST_BUCKETS_USD = (1e-5, 3e-5, 1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1, 3e-1, 1.0, float("inf"))
COST_PER_1K_TOKENS_BUCKETS_USD = (1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1, 3e-1, 1.0, float("inf"))

12 y 10 buckets respectivamente, cubriendo 5 décadas. Espaciados en log porque el coste está sesgado a la derecha.

Dos etiquetas en cada uno: model_name y kind (prompt vs completion para el coste por token). Ninguna etiqueta más — presupuesto de cardinalidad.

Consultas PromQL que Borja realmente escribirá

Las cuatro consultas que deberían estar en el panel de coste del dashboard:

# Coste medio por petición, últimos 5 minutos:
sum(rate(cost_per_request_usd_sum[5m])) / sum(rate(cost_per_request_usd_count[5m]))

# p95 coste por 1k tokens de salida, últimos 5 minutos:
histogram_quantile(0.95, sum by (le) (rate(cost_per_1k_completion_tokens_usd_bucket[5m])))

# Gasto total, últimas 24 horas:
sum(increase(cost_per_request_usd_sum[24h]))

# Distribución coste-por-petición como heatmap (tipo de panel Grafana):
sum by (le) (rate(cost_per_request_usd_bucket[1m]))

La tercera — gasto total — es la línea FinOps que se pone delante de la dirección. La segunda — p95 coste por 1k tokens — es el KPI de ingeniería.

La trampa: el coste sin calidad no tiene sentido

Un modelo que siempre devuelve "no lo sé" es infinitamente eficiente en coste. El coste tiene que reportarse junto a alguna señal de calidad — incluso una burda como "% de respuestas que parsearon como JSON válido" (Fase 30) o "% de respuestas que pasaron el filtro de seguridad" (Fase 37). La Fase 34 pone el número de coste; la Fase 38 lo cablea al número de calidad en el mismo dashboard.

Para el laboratorio en la Fase 34: commitea un panel "coste por completion exitoso" donde successful se define como status==200 and completion_tokens > 0. Burdo, pero previene el degenerado juego del "siempre 503".

Ejemplo trabajado: ¿qué pinta debería tener el coste para el MiniGPT de Borja?

Estimaciones de orden de magnitud para un i5-8250U sirviendo el MiniGPT de ~500k parámetros en FP32:

  • TTFT para un prompt de 128 tokens: ~50 ms.
  • Decode por token: ~10 ms.
  • Una completion de 256 tokens tarda ~50 + 256×10 = 2610 ms.
  • A tarifa $0.17/hr: 2.61 × \(0.17/3600 = **\)1.23e-4 por petición, o $0.48 por 1000 peticiones**.
  • Por 1k tokens de completion: (\(1.23e-4 / 256) × 1000 = **\)4.8e-4 por 1k tokens**.

Comprobación de cordura: gpt-4o-mini de OpenAI está a ~\(0.15/1M tokens de completion, o **\)1.5e-4 por 1k tokens. Así que un MiniGPT en CPU hecho a mano, en un portátil de 2018, con una tarifa cloud nocional, es aproximadamente 3×** más caro por 1k tokens que la API de producción más barata. Esa es una respuesta razonable para un sistema educativo — no tan barato como para ser sospechoso, no tan caro como para ser embarazoso. Borja debería aterrizar más o menos en este rango.

Si el número medido acaba en $0.01 por 1k tokens (20× desviado), algo va mal: lo más probable es que el batching del batcher no se esté acreditando, o la tarifa esté puesta demasiado alta, o el modelo esté corriendo inútilmente lento.

Recapitulación en un párrafo

Coste por petición = tiempo de reloj × tarifa de la máquina, con tres correcciones: el solapamiento por batch divide el coste de decode entre peticiones activas, el prefill cuadrático aparece automáticamente en el tiempo de reloj pero importa para la cola de prompts largos, el retrieval es pequeño pero se rastrea por separado. Codifícalo como dos histogramas de Prometheus espaciados en log (cost_per_request_usd, cost_per_1k_completion_tokens_usd) etiquetados por nombre de modelo. El coste p95 es el KPI de ingeniería; el gasto total es la línea FinOps. Empareja siempre el coste con una señal de calidad para que los servicios degenerados de "rechazar todo" no se lean como victorias.


Siguiente: theory/03-tracing-and-logging.md.