Chargeback y showback de GPU en multi-tenancy: cómo repartir el coste del cluster entre equipos
Notación: importes en euros (N €), decimales con coma. No se usa el símbolo de dólar (en este sitio es delimitador de fórmula).
TL;DR
Un cluster de 4×H100 SXM a ~1,40 €/GPU-hora (capex amortizado + energía) compartido entre tres equipos con distinta ocupación produce un informe de chargeback mensual de 3 filas y una fila adicional de idle que nadie reclamó. Sin herramientas, ese coste ocioso desaparece diluido en el total. Con OpenCost + LiteLLM + Kueue, la atribución opera en tres planos ortogonales: el hierro (OpenCost, €/GPU-hora por namespace/label), el consumo de tokens (LiteLLM, €/token por clave/equipo/modelo) y la cuota de scheduler (Kueue, GPUs reservadas por ClusterQueue). El cruce de los tres produce el número que va a finanzas: equipo B consumió X millones de tokens a Y €/1M tok, la GPU le costó Z €, y todavía tiene 2 GPUs prestadas del cohort que le costarán W € si las retiene el mes que viene.
Showback vs chargeback: definición FinOps Foundation
La distinción no es de tecnología sino de formalidad contable (FinOps Foundation — Invoicing & Chargeback, FinOps Foundation — Data Analysis and Showback):
| Concepto | Definición | Mueve dinero | Requiere |
|---|---|---|---|
| Showback | visibilidad del consumo y su coste por equipo; el informe llega, el presupuesto no cambia | No | métricas + atribución |
| Chargeback | el coste se transfiere al P&L del equipo o producto como gasto real | Sí | showback + política contable + sistema financiero |
Dos puntos del framework que conviene fijar:
- Showback es requisito de cualquier práctica FinOps; chargeback es opcional y depende de que la política contable de la organización soporte la transferencia entre centros de coste.
- Ninguno es “más maduro” que el otro. La narrativa de que chargeback es la versión “adulta” es falsa según el propio framework. La secuencia natural es: showback → confianza en los datos → chargeback si la política lo permite.
Cuándo usar cada uno
| Situación | Modalidad recomendada |
|---|---|
| Primer ciclo de atribución; los equipos aún no confían en los datos | Showback |
| Equipos con presupuesto propio en sistema financiero | Chargeback |
| Cluster compartido sin separación de P&L por equipo | Showback con coste de idle visible |
| Equipos con SLA de disponibilidad de GPU comprometido | Chargeback (reserva real) |
| Workloads de investigación/experimentación sin presupuesto formal | Showback + alerta de umbral |
Atribución del coste GPU con OpenCost
Allocation API: parámetros de atribución
La API /allocation de OpenCost es la pieza que transforma las métricas de Prometheus en un
informe de coste por dimensión
(OpenCost — API):
| Parámetro | Valores | Uso en multi-tenancy |
|---|---|---|
window | today, 7d, lastmonth, rango RFC3339 | ventana del informe mensual |
aggregate | namespace, label:LABEL, annotation:NAME, pod, controller | dimensión de atribución |
includeIdle | true / false | añade fila __idle__ al informe |
shareIdle | true / false | distribuye el idle entre las asignaciones no ociosas (proporcional a coste no idle) |
idleByNode | true / false | calcula idle por nodo en lugar de por cluster |
filter | namespace:"llm-prod", label:equipo:"datos" | limitar a un equipo concreto |
Consulta de informe mensual por equipo, con idle visible como fila separada:
curl -G http://localhost:9003/allocation \
-d window=lastmonth \
-d aggregate=label:equipo \
-d includeIdle=true \
-d shareIdle=false \
-d resolution=10m
Con shareIdle=false (el default), OpenCost devuelve el idle como __idle__ separado — lo que
hace visible cuánto se pagó por capacidad sin usar. Con shareIdle=true, ese idle se distribuye
entre los tenants en proporción a su coste no-idle: cada uno absorbe su parte del desperdicio,
lo que es correcto para un chargeback que penalice al responsable del idle.
Etiquetado de pods de inferencia
Para que aggregate=label:equipo funcione, los pods de inferencia necesitan el label correcto.
Dos métodos:
Label en el pod spec (el más directo):
# Deployment de vLLM para equipo datos
metadata:
labels:
equipo: datos
producto: rag-prod
modelo: llama-70b
entorno: prod
Annotation (cuando el label ya está ocupado por otra convención):
# agrega como annotation
aggregate=annotation:finops.io/equipo
OpenCost soporta aggregate=label:KEY y aggregate=annotation:KEY con la misma sintaxis; la
elección entre uno u otro depende de la convención de etiquetado del cluster.
Costes compartidos e idle: los tres modos
| Modo API | Comportamiento | Cuándo |
|---|---|---|
includeIdle=false (default) | Ignora el idle; el coste total parece menor de lo que es | Nunca en producción |
includeIdle=true, shareIdle=false | Idle en fila __idle__ separada | Showback: visibilidad del desperdicio |
includeIdle=true, shareIdle=true | Idle distribuido entre tenants proporcionalmente | Chargeback: el equipo paga su parte del idle |
shareIdle=true, idleByNode=true | Idle distribuido por nodo (más granular si hay nodos dedicados) | Chargeback con nodos heterogéneos |
Para infraestructura con nodos dedicados por equipo, idleByNode=true es más justo: el idle de
un nodo dedicado al equipo A no se transfiere al equipo B.
LiteLLM como punto de atribución por consumo de tokens
Virtual keys y equipos
LiteLLM materializa el chargeback por tokens con cuatro mecanismos (LiteLLM — Virtual Keys, LiteLLM — Setting Team Budgets, LiteLLM — Spend Tracking):
| Mecanismo | Qué hace |
|---|---|
Virtual key con max_budget | presupuesto mensual por clave; la key se bloquea al agotarlo |
budget_duration | ventana de reset: 30d, 7d, 24h; el proxy corre un cron diario que resetea según la duración |
team_id | agrupa claves; el gasto se acumula en LiteLLM_TeamTable por equipo |
tags | etiquetan cada request; permiten presupuestos por tag (centro de coste) |
| Spend logs | una fila por petición con team_id, model, prompt_tokens, completion_tokens, response_cost |
Configuración de equipos con presupuesto mensual
# litellm-config.yaml — presupuestos por equipo con coste on-prem declarado
model_list:
- model_name: llama-3-70b-onprem
litellm_params:
model: openai/llama-3-70b
api_base: http://vllm-svc:8000/v1
input_cost_per_token: 0.00000140 # 1,40 €/1M tok (€/GPU-hora ÷ throughput)
output_cost_per_token: 0.00000140
- model_name: llama-3-8b-onprem
litellm_params:
model: openai/llama-3-8b
api_base: http://vllm-8b-svc:8000/v1
input_cost_per_token: 0.00000035 # 0,35 €/1M tok (1 GPU, mayor throughput)
output_cost_per_token: 0.00000035
litellm_settings:
success_callback: ["langfuse"] # o cualquier logger externo
# Equipos (vía API /team/new o config)
# POST /team/new
# {
# "team_alias": "equipo-datos",
# "max_budget": 800, # 800 € al mes
# "budget_duration": "30d",
# "tpm_limit": 500000, # tokens/min máx
# "rpm_limit": 1000 # requests/min máx
# }
La clave input_cost_per_token sale de la identidad del artículo A4:
el €/GPU-hora de OpenCost dividido por el throughput del benchmark. Con ese valor, el
response_cost de cada petición refleja el coste real del hierro on-prem para ese modelo.
Endpoints de consulta de gasto por equipo
# Gasto acumulado del equipo en el periodo
GET /team/info?team_id=equipo-datos
# Gasto por modelo del equipo (tabla spend_logs agregada)
GET /global/spend/logs?team_id=equipo-datos&start_date=2026-06-01&end_date=2026-06-30
# Gasto total por todos los equipos
GET /global/spend/teams
La respuesta incluye spend (coste acumulado en la divisa configurada), total_tokens,
prompt_tokens, completion_tokens y desglose por modelo — los campos directos para el
informe de chargeback mensual.
Unir €/GPU-hora (hierro) con tokens por equipo
Los dos planos de atribución — OpenCost (hierro) y LiteLLM (tokens) — se cruzan con una clave:
el model_name y el namespace. El join se hace fuera de las herramientas (en un pipeline de
BI o una consulta SQL sobre la base de datos de LiteLLM y los datos exportados de OpenCost):
coste_por_token_real = OpenCost.GPU_cost_namespace / LiteLLM.total_tokens_team
Cuando el coste on-prem está bien declarado en input_cost_per_token, LiteLLM ya hace este
cálculo internamente y el response_cost es correcto. El join explícito sirve para validar: si
la suma de response_cost de LiteLLM por equipo no coincide con el coste GPU asignado por
OpenCost al namespace, hay una deriva en el input_cost_per_token que hay que corregir
(throughput cambiado, optimización nueva, más réplicas).
Kueue como mecanismo de presupuesto/cuota de GPU
ClusterQueue, LocalQueue y cohorts
Kueue introduce la capa de scheduling con cuotas sobre Kubernetes (Kueue — Cluster Queue, Kueue — Cohort, Kueue — Fair Sharing):
| Objeto | Alcance | Qué hace |
|---|---|---|
ResourceFlavor | cluster | mapea recursos a un grupo de nodos (ej.: nodos H100) |
ClusterQueue | cluster | define nominalQuota, borrowingLimit, lendingLimit por recurso/flavor |
LocalQueue | namespace | punto de entrada para los workloads del equipo; apunta a un ClusterQueue |
Cohort | cluster | agrupa ClusterQueues que pueden prestarse quota entre sí |
Conceptos de quota
nominalQuota: GPUs garantizadas al ClusterQueue en todo momento.borrowingLimit: GPUs adicionales máximas que puede tomar prestadas del cohort cuando otros no las usan.lendingLimit: GPUs de sunominalQuotaque permite prestar a otros (si no se especifica, puede prestar todas las no usadas).- Fair Sharing: mecanismo que ordena los workloads pendientes por uso histórico de recursos de su LocalQueue, dando preferencia a quien ha consumido menos. Compatible con cohorts jerárquicos desde Kueue v0.11.
YAML de ejemplo: 3 equipos en cohort llm-platform
# ResourceFlavor que mapea a los nodos H100 SXM
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: h100-sxm
spec:
nodeLabels:
accelerator: h100-sxm
---
# ClusterQueue equipo datos — 2 GPUs garantizadas, puede pedir hasta 2 prestadas
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: cq-datos
spec:
cohort: llm-platform
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ns-datos
queueingStrategy: BestEffortFIFO
resourceGroups:
- coveredResources: ["nvidia.com/gpu"]
flavors:
- name: h100-sxm
resources:
- name: "nvidia.com/gpu"
nominalQuota: 2
borrowingLimit: 2 # puede usar hasta 4 GPUs en total
preemption:
reclaimWithinCohort: LowerPriority
withinClusterQueue: LowerPriority
---
# ClusterQueue equipo ia — 1 GPU garantizada, presta lo que no usa
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: cq-ia
spec:
cohort: llm-platform
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ns-ia
resourceGroups:
- coveredResources: ["nvidia.com/gpu"]
flavors:
- name: h100-sxm
resources:
- name: "nvidia.com/gpu"
nominalQuota: 1
lendingLimit: 1 # presta su GPU cuando no la usa
---
# ClusterQueue equipo plataforma — 1 GPU garantizada, prioridad baja
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: cq-plataforma
spec:
cohort: llm-platform
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ns-plataforma
resourceGroups:
- coveredResources: ["nvidia.com/gpu"]
flavors:
- name: h100-sxm
resources:
- name: "nvidia.com/gpu"
nominalQuota: 1
borrowingLimit: 3 # puede usar las 4 si el resto está libre
---
# LocalQueue en cada namespace — punto de entrada para los workloads
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
name: lq-datos
namespace: ns-datos
spec:
clusterQueue: cq-datos
Cómo Kueue materializa el “presupuesto de GPU”
| Concepto FinOps | Mecanismo Kueue |
|---|---|
| Presupuesto garantizado | nominalQuota: el equipo siempre tiene estas GPUs disponibles |
| Límite de gasto máximo | nominalQuota + borrowingLimit: techo absoluto de GPUs admisibles |
| Préstamo de capacidad ociosa | cohort + lendingLimit: otros toman lo que este no usa |
| Fair sharing entre equipos | Fair Sharing + WorkloadPriorityClass: los que más han consumido esperan más |
| Recuperar la cuota propia | preemption.reclaimWithinCohort: LowerPriority: el dueño de la GPU prestada la recupera expulsando workloads de menor prioridad |
El nominalQuota es la expresión del presupuesto en GPUs: si el equipo tiene 2 GPUs nominales
y el precio es 1,40 €/GPU-hora, su gasto máximo garantizado es
[ \text{gasto máximo nominal} = 2 \times 1{,}40 \times 720 = 2,016 \text{ €/mes} ]
El borrowingLimit fija el sobrecoste potencial si toma prestado del cohort (y ese préstamo se
puede imputar con la misma fórmula multiplicando las horas de uso prestado por 1,40 €/GPU-hora).
Modelo de coste: ejemplo con 4×H100 SXM y 3 equipos
Precio del nodo
Hardware genérico: servidor con 4×H100 SXM 80 GB. Precios de mercado 2026: una H100 SXM individual cuesta entre 27.000 y 40.000 € según fuente (GMI Cloud — H100 GPU Pricing 2026); un servidor 4×H100 se sitúa en el rango 120.000–180.000 € incluyendo chasis, fuentes y NVLink. Usamos 140.000 € como supuesto genérico para el servidor completo (4 GPUs + infraestructura).
| Componente | Cálculo | €/hora nodo |
|---|---|---|
| Capex amortizado (140.000 €, 4 años, 90 % disponibilidad) | 140.000 ÷ (4 × 8.760 × 0,9) | ~4,43 |
| Energía (4 × 700 W TDP × PUE 1,4 × 0,12 €/kWh) | 4 × 0,7 × 1,4 × 0,12 | ~0,47 |
| Operación / red / mantenimiento | estimación | ~0,70 |
| Total nodo 4×H100 | ~5,60 €/h | |
| Por GPU | 5,60 ÷ 4 | ~1,40 €/GPU-hora |
Fórmula del coste por token para el modelo de 70B (throughput sostenido ~2.000 tok/s en TP=4):
$$\text{€/1M tokens} = \frac{5{,}60 \text{ €/h}}{2{,}000 \text{ tok/s} \times 3,600 \text{ s/h}} \times 10^6 \approx 0{,}78 \text{ €/1M tokens}$$
Escenario mensual: 3 equipos, utilización heterogénea
Mes de referencia (720 horas). Las 4 GPUs están nominalmente asignadas: 2 a Datos, 1 a IA, 1 a Plataforma. La ocupación real varía:
| Equipo | GPUs nominales | Ocupación media | GPU-horas usadas | GPU-horas ociosas | Coste GPU (€) |
|---|---|---|---|---|---|
| Datos | 2 | 75 % | 1.080 | 360 | ~1.512 |
| IA | 1 | 60 % | 432 | 288 | ~605 |
| Plataforma | 1 | 35 % | 252 | 468 | ~353 |
| Idle cluster | — | — | — | 1.116 | ~1.562 |
| Total nodo | 4 | — | 1.764 | 1.116 | ~4.032 |
Coste total del nodo al mes: 5,60 €/h × 720 h = 4.032 €. Coste por GPU-hora usada: 5,60 ÷ 4 = 1,40 €.
Con shareIdle=false (showback), el idle de 1.562 € aparece como fila separada en el informe
y nadie lo paga directamente. Con shareIdle=true (chargeback proporcional), se reparte entre
los tres en proporción a su coste no-idle: Datos absorbe ~857 €, IA ~333 €, Plataforma ~192 €
de idle adicional.
Consumo de tokens por equipo (LiteLLM spend logs)
| Equipo | Modelo | Tokens/mes | Coste/token | Coste tokens (€) |
|---|---|---|---|---|
| Datos | llama-70b (TP=4) | 380 M | 0,78 €/1M | ~296 |
| IA | llama-8b (1 GPU) | 210 M | 0,35 €/1M | ~74 |
| Plataforma | llama-8b + batch | 80 M | 0,35 €/1M | ~28 |
Informe de chargeback mensual (showback + tokens)
| Equipo | Coste GPU hierro (€) | Coste idle asignado (€) | Coste tokens LiteLLM (€) | Total imputable (€) |
|---|---|---|---|---|
| Datos | 1.512 | 857 | 296 | 2.665 |
| IA | 605 | 333 | 74 | 1.012 |
| Plataforma | 353 | 192 | 28 | 573 |
| Idle (sin shareIdle) | 1.562 | — | — | visible, no imputado |
Tabla de dimensiones de atribución × política
| Dimensión de atribución | Herramienta | Showback | Chargeback |
|---|---|---|---|
| namespace | OpenCost aggregate=namespace | /allocation?aggregate=namespace&includeIdle=true | shareIdle=true + transfer a P&L |
| label de pod | OpenCost aggregate=label:equipo | informe por label | idem con shareIdle |
| virtual key / team | LiteLLM /global/spend/teams | dashboard de tokens | max_budget duro por clave |
| ClusterQueue (GPU reservada) | Kueue nominalQuota | observar uso vs quota | presupuesto en GPUs comprometido |
| tag de request | LiteLLM tag_budgets | por centro de coste | presupuesto por tag |
| annotation de pod | OpenCost aggregate=annotation:KEY | informe por annotation | idem con shareIdle |
Flujo de datos end-to-end
Fair sharing y preemption: cómo Kueue recupera la cuota
Cuando el equipo Datos ha agotado sus 2 GPUs nominales y toma prestadas las de Plataforma (que no las usa), Kueue registra ese préstamo. En cuanto Plataforma lanza un workload nuevo:
- Kueue detecta que
cq-plataformaestá por debajo de sunominalQuota. - Con
preemption.reclaimWithinCohort: LowerPriority, Kueue expulsa el workload de Datos que estaba en la GPU prestada, si su prioridad es menor. - El workload expulsado vuelve a la cola de
cq-datosy se readmite cuando se libere cuota.
El Fair Sharing ordena la cola de pendientes por uso histórico acumulado de la LocalQueue: si Datos ha consumido más GPU-horas que IA en el periodo reciente, los próximos workloads de IA tienen prioridad de admisión antes que los nuevos de Datos. Esto implementa un reparto equitativo sin bloquear a nadie permanentemente.
Configuración de OpenCost para el precio del nodo
Para que los números de la sección anterior sean reproducibles, el precio del nodo hay que declararlo explícitamente (el valor por defecto subestima GPU on-prem, issue #3781):
# values.yaml Helm de OpenCost — nodo 4×H100 on-prem, 5,60 €/h
opencost:
customPricing:
enabled: true
provider: custom
costModel:
description: "Nodo 4xH100 SXM on-prem"
CPU: "0.025" # €/CPU-hora (componente menor)
RAM: "0.003" # €/GB-hora
GPU: "1.40" # €/GPU-hora ← el que mueve el reparto
storage: "0.0002" # €/GB-hora
Verificar tras configurar:
# Comprobar precio resuelto por nodo
curl http://localhost:9003/allNodePricing
# Consulta de atribución por label de equipo, último mes, idle separado
curl -G http://localhost:9003/allocation \
-d window=lastmonth \
-d aggregate=label:equipo \
-d includeIdle=true \
-d shareIdle=false \
-d accumulate=true
PromQL de referencia para paneles de chargeback
# Coste GPU asignado por equipo (label:equipo) — €/hora
sum by (label_equipo) (
container_gpu_allocation
* on(node) group_left node_gpu_hourly_cost
)
# GPU-horas ociosas por nodo (idle > 15 min)
sum by (node) (
(1 - avg_over_time(DCGM_FI_DEV_GPU_UTIL[15m]) / 100)
* on(node) group_left node_gpu_hourly_cost
)
# Coste acumulado del mes por equipo (suma sobre ventana)
sum_over_time(
sum by (label_equipo) (
container_gpu_allocation * on(node) group_left node_gpu_hourly_cost
)[30d:1h]
)
Fuentes
- FinOps Foundation — Invoicing & Chargeback Capability — https://www.finops.org/framework/capabilities/invoicing-chargeback/
- FinOps Foundation — Data Analysis and Showback — https://www.finops.org/framework/previous-capabilities/analysis-showback/
- FinOps Foundation — Allocation Capability — https://www.finops.org/framework/capabilities/allocation/
- OpenCost — API (Allocation API, parámetros window/aggregate/shareIdle/idleByNode) — https://opencost.io/docs/integrations/api/
- OpenCost — API Examples — https://opencost.io/docs/integrations/api-examples/
- OpenCost — GitHub (issue #3781, infra-precio GPU on-prem por defecto) — https://github.com/opencost/opencost/issues/3781
- LiteLLM — Virtual Keys (
max_budget,budget_duration,team_id) — https://docs.litellm.ai/docs/proxy/virtual_keys - LiteLLM — Setting Team Budgets — https://docs.litellm.ai/docs/proxy/team_budgets
- LiteLLM — Spend Tracking — https://docs.litellm.ai/docs/proxy/cost_tracking
- LiteLLM — Budgets & Rate Limits — https://docs.litellm.ai/docs/proxy/users
- LiteLLM — Setting Tag Budgets — https://docs.litellm.ai/docs/proxy/tag_budgets
- LiteLLM — Budget Reset Times — https://docs.litellm.ai/docs/proxy/budget_reset_and_tz
- Kueue — Cluster Queue (nominalQuota, borrowingLimit, lendingLimit, preemption) — https://kueue.sigs.k8s.io/docs/concepts/cluster_queue/
- Kueue — Cohort — https://kueue.sigs.k8s.io/docs/concepts/cohort/
- Kueue — Fair Sharing — https://kueue.sigs.k8s.io/docs/concepts/fair_sharing/
- Kueue — Administer Cluster Quotas — https://kueue.sigs.k8s.io/docs/tasks/manage/administer_cluster_quotas/
- GMI Cloud — NVIDIA H100 GPU Pricing 2026 — https://www.gmicloud.ai/en/blog/nvidia-h100-gpu-pricing-2026-rent-vs-buy-cost-analysis
- IntuitionLabs — NVIDIA AI GPU Prices H100 Cost Guide — https://intuitionlabs.ai/articles/nvidia-ai-gpu-pricing-guide