1 Versiones de librerías utilizadas

Este documento utiliza exclusivamente R base (sin librerías de redes neuronales ni diferenciación automática). Todas las funciones empleadas están definidas dentro del propio documento.

# ── Reporte de versiones ─────────────────────────────────────────────────────
cat(sprintf("R versión  : %s\n", R.version$version.string))
## R versión  : R version 4.5.3 (2026-03-11 ucrt)
cat(sprintf("Plataforma : %s\n", R.version$platform))
## Plataforma : x86_64-w64-mingw32
cat(sprintf("Fecha      : %s\n", format(Sys.Date(), "%Y-%m-%d")))
## Fecha      : 2026-05-15
cat(sprintf("Paquetes   : R base únicamente (graphics, stats, utils)\n"))
## Paquetes   : R base únicamente (graphics, stats, utils)
cat(sprintf("stats      : %s\n", packageVersion("stats")))
## stats      : 4.5.3
cat(sprintf("graphics   : %s\n", packageVersion("graphics")))
## graphics   : 4.5.3

2 Introducción

DeepSeek-V2 [@deepseekv2_2024] es un modelo de lenguaje de gran escala basado en la arquitectura Mixture-of-Experts (MoE), publicado por DeepSeek-AI en mayo de 2024 (arXiv:2405.04434). Su objetivo principal es demostrar que la innovación arquitectónica puede superar al simple escalado de parámetros, logrando rendimiento de punta con un costo computacional drásticamente menor.

El modelo cuenta con 236 mil millones de parámetros totales, pero activa solo 21 mil millones por token gracias a dos innovaciones complementarias:

  1. Multi-head Latent Attention (MLA): reduce el KV cache en un 93.3 % frente a la atención multi-head estándar (MHA).
  2. DeepSeekMoE: reduce el costo de entrenamiento en un 42.5 % frente a DeepSeek 67B denso.
# ── Figura 1: Contribuciones clave ───────────────────────────────────────────
set.seed(42)  # semilla fija (chunk sin aleatoriedad; se fija por consistencia)

metricas <- c(
  "Reducción\nKV cache",
  "Ahorro\nentrenamiento",
  "Speedup\ngeneración\n(×5.76 = 476%)",
  "MMLU sobre\nDeepSeek 67B"
)
valores  <- c(93.3, 42.5, 476.0, 10.1)
colores  <- c(COL_CYAN, COL_GREEN, COL_BLUE, COL_YELLOW)

par(mar = c(6, 5, 3, 2))
bp <- barplot(
  valores,
  names.arg = metricas,
  col       = colores,
  border    = "white",
  ylim      = c(0, 560),
  ylab      = "Magnitud de la mejora (%)",
  main      = "DeepSeek-V2 — Contribuciones clave (arXiv:2405.04434)",
  las       = 1,
  cex.names = 0.82,
  cex.axis  = 0.85
)
text(bp, valores + 12, paste0(valores, "%"), cex = 0.88, font = 2, col = colores)
grid(nx = NA, ny = NULL, col = "gray90", lty = 1)
Figura 1. Magnitud de las mejoras reportadas en DeepSeek-V2 respecto al modelo predecesor (DeepSeek 67B). Las barras muestran el porcentaje de mejora en cada dimensión clave.

Figura 1. Magnitud de las mejoras reportadas en DeepSeek-V2 respecto al modelo predecesor (DeepSeek 67B). Las barras muestran el porcentaje de mejora en cada dimensión clave.

La Figura 1 muestra que la reducción del KV cache (93.3 %) y el ahorro en entrenamiento (42.5 %) son las ganancias más directamente relacionadas con las innovaciones arquitectónicas, mientras que el speedup de generación refleja el impacto práctico en despliegue.


3 Multi-Head Latent Attention (MLA)

3.1 El problema: KV cache en la atención estándar

En la atención multi-cabeza estándar (MHA), durante la inferencia autoregresiva se almacenan en caché las matrices de claves \(K\) y valores \(V\) para cada token previo. El tamaño del caché crece linealmente con la longitud de la secuencia \(\ell\):

\[\text{KV\_cache}_{\text{MHA}} = 2 \times n_h \times d_h \times \ell \times \text{bytes}\]

Para DeepSeek-V2 con \(n_h = 128\), \(d_h = 128\), formato BF16 (2 bytes) y contexto de 128 K tokens, esto equivale a 8.39 GB solo de KV cache.

# ── Tabla 1: Reducción del KV cache ─────────────────────────────────────────
set.seed(42)

n_h  <- 128L;  d_h  <- 128L
d_c  <- 512L;  d_R  <- 64L    # dimensiones MLA de DeepSeek-V2

seq_lens <- c(1000, 4000, 8000, 16000, 32000, 64000, 128000)

mha_gb  <- 2 * n_h * d_h * seq_lens * 2 / 1e9   # BF16 = 2 bytes
mla_gb  <- (d_c + d_R) * seq_lens * 2 / 1e9
red_pct <- (1 - mla_gb / mha_gb) * 100

tbl_kv <- data.frame(
  `Secuencia (tokens)` = format(seq_lens, big.mark = ","),
  `MHA cache (GB)`     = round(mha_gb, 3),
  `MLA cache (GB)`     = round(mla_gb, 5),
  `Reducción (%)`      = round(red_pct, 1),
  check.names          = FALSE
)

knitr::kable(
  tbl_kv,
  caption = "Tabla 1. Tamaño del KV cache (BF16) según longitud de secuencia para MHA estándar vs MLA de DeepSeek-V2 (n_h=128, d_h=128, d_c=512, d_R=64).",
  align   = c("r", "r", "r", "r")
)
Tabla 1. Tamaño del KV cache (BF16) según longitud de secuencia para MHA estándar vs MLA de DeepSeek-V2 (n_h=128, d_h=128, d_c=512, d_R=64).
Secuencia (tokens) MHA cache (GB) MLA cache (GB) Reducción (%)
1,000 0.066 0.0012 98.2
4,000 0.262 0.0046 98.2
8,000 0.524 0.0092 98.2
16,000 1.049 0.0184 98.2
32,000 2.097 0.0369 98.2
64,000 4.194 0.0737 98.2
128,000 8.389 0.1475 98.2

La Tabla 1 confirma que MLA reduce el KV cache en un 98.2 % de manera uniforme para cualquier longitud de secuencia, lo que se traduce en una reducción de 8.39 GB a solo 0.15 GB en el contexto máximo de 128 K tokens.

# ── Figura 2: Escalado del KV cache ─────────────────────────────────────────
set.seed(42)

seq_plot <- seq(1000, 128000, by = 1000)
mha_plot <- 2 * n_h * d_h * seq_plot * 2 / 1e9
mla_plot <- (d_c + d_R) * seq_plot * 2 / 1e9

par(mar = c(5, 5, 3, 2))
plot(
  seq_plot / 1000, mha_plot,
  type = "l", col = COL_RED, lwd = 2.5,
  xlab = "Longitud de secuencia (× 1 000 tokens)",
  ylab = "KV cache (GB, BF16)",
  main = "Escalado del KV cache: MHA vs MLA",
  ylim = c(0, max(mha_plot) * 1.05),
  las  = 1
)
polygon(
  c(seq_plot / 1000, rev(seq_plot / 1000)),
  c(mha_plot, rep(0, length(mha_plot))),
  col = adjustcolor(COL_RED, alpha.f = 0.08), border = NA
)
lines(seq_plot / 1000, mla_plot, col = COL_CYAN, lwd = 2.5)
polygon(
  c(seq_plot / 1000, rev(seq_plot / 1000)),
  c(mla_plot, rep(0, length(mla_plot))),
  col = adjustcolor(COL_CYAN, alpha.f = 0.12), border = NA
)
abline(v = 128, col = COL_YELLOW, lty = 2, lwd = 1.8)
text(120, max(mha_plot) * 0.88, "128 K\n(máx.)",
     col = COL_YELLOW, cex = 0.78, adj = 1)
legend(
  "topleft",
  legend = c("MHA estándar", "MLA (DeepSeek-V2)"),
  col    = c(COL_RED, COL_CYAN),
  lwd    = 2.5, lty = 1, bty = "n", cex = 0.88
)
grid(col = "gray90")
Figura 2. Escalado del KV cache (GB, BF16) con la longitud del contexto para los parámetros reales de DeepSeek-V2. La línea punteada indica el contexto máximo soportado (128 K tokens).

Figura 2. Escalado del KV cache (GB, BF16) con la longitud del contexto para los parámetros reales de DeepSeek-V2. La línea punteada indica el contexto máximo soportado (128 K tokens).

La Figura 2 ilustra cómo el problema del KV cache con MHA crece de forma lineal y alcanza 8.39 GB en 128 K tokens, mientras que MLA mantiene la memoria por debajo de 0.15 GB durante todo el rango.

3.2 Formulación matemática de MLA

La idea central es comprimir conjuntamente \(K\) y \(V\) en un vector latente de baja dimensión \(c^{KV}_t\):

\[c^{KV}_t = W^{DKV} h_t \qquad (d_c = 512 \ll n_h \cdot d_h = 16\,384)\]

Las representaciones completas se recuperan mediante:

\[K^C = W^{UK} c^{KV}_t, \qquad V = W^{UV} c^{KV}_t, \qquad Q = W^Q h_t\]

Solo \(c^{KV}_t\) (512 valores) se almacena por token, en lugar de \(K\) y \(V\) completos (16 384 valores). La reducción es \(1 - 512/16384 = 96.9\%\); con el RoPE desacoplado (\(d_R = 64\)), el caché total es \(512 + 64 = 576\) valores, para una reducción del 96.5 %.

3.3 Implementación en R: forward pass MLA

# ── Forward pass MLA simplificado ───────────────────────────────────────────
set.seed(42)   # semilla fija

d_model <- 256L;  n_heads <- 8L;  d_head <- d_model %/% n_heads  # = 32
d_c2    <- 64L    # dimensión de compresión latente
seq_len <- 10L

# Estados ocultos de entrada
H <- matrix(rnorm(seq_len * d_model), nrow = seq_len, ncol = d_model)

# Matrices de proyección (aprendidas; aquí inicializadas aleatoriamente)
W_DKV <- matrix(rnorm(d_model * d_c2),            nrow = d_model, ncol = d_c2)
W_UK  <- matrix(rnorm(d_c2  * n_heads * d_head),  nrow = d_c2,   ncol = n_heads * d_head)
W_UV  <- matrix(rnorm(d_c2  * n_heads * d_head),  nrow = d_c2,   ncol = n_heads * d_head)
W_Q   <- matrix(rnorm(d_model * n_heads * d_head), nrow = d_model, ncol = n_heads * d_head)

# ── Paso 1: Comprimir K y V → vector latente (SOLO esto se almacena en caché)
c_KV <- H %*% W_DKV          # (seq_len × d_c2)

# ── Paso 2: Recuperar K y V desde c_KV ──────────────────────────────────────
K <- c_KV %*% W_UK            # (seq_len × n_heads*d_head)
V <- c_KV %*% W_UV            # (seq_len × n_heads*d_head)
Q <- H    %*% W_Q             # (seq_len × n_heads*d_head)

# ── Paso 3: Softmax 2D numéricamente estable ─────────────────────────────────
softmax_2d <- function(x) {
  # Aplica softmax fila a fila de forma numéricamente estable
  x_shifted <- x - apply(x, 1, max)
  e         <- exp(x_shifted)
  e / rowSums(e)
}

# ── Paso 4: Atención estándar ────────────────────────────────────────────────
scale   <- sqrt(d_head)
scores  <- (Q %*% t(K)) / scale    # (seq_len × seq_len)
attn_w  <- softmax_2d(scores)      # pesos normalizados
output  <- attn_w %*% V            # (seq_len × n_heads*d_head)

# ── Métricas de memoria ──────────────────────────────────────────────────────
cache_mha <- 2L * n_heads * d_head * seq_len
cache_mla <- d_c2 * seq_len

cat(sprintf("Dimensión de salida    : (%d, %d)\n", nrow(output), ncol(output)))
## Dimensión de salida    : (10, 256)
cat(sprintf("Cache MHA estándar     : %6d valores\n", cache_mha))
## Cache MHA estándar     :   5120 valores
cat(sprintf("Cache MLA (c_KV)       : %6d valores\n", cache_mla))
## Cache MLA (c_KV)       :    640 valores
cat(sprintf("Reducción de memoria   : %5.1f%%\n",
            (1 - cache_mla / cache_mha) * 100))
## Reducción de memoria   :  87.5%
# ── Figura 3: Heatmap de pesos de atención ───────────────────────────────────

# Paleta degradada de blanco a azul oscuro
n_col  <- 100
pal    <- colorRampPalette(c("#F0F4FB", "#2471A3", "#1F3864"))(n_col)
breaks <- seq(0, max(attn_w) * 1.01, length.out = n_col + 1)

par(mar = c(4.5, 4.5, 3, 1))
image(
  x    = 1:seq_len,
  y    = 1:seq_len,
  z    = t(attn_w),          # transponer para orientación estándar
  col  = pal,
  breaks = breaks,
  xlab = "Posición de la clave (Key)",
  ylab = "Posición de la consulta (Query)",
  main = "Pesos de atención — MLA (seq_len = 10)",
  las  = 1
)
# Añadir valores en las celdas
for (i in 1:seq_len)
  for (j in 1:seq_len)
    text(j, i, round(attn_w[i, j], 2), cex = 0.55,
         col = if (attn_w[i, j] > 0.15) "white" else "gray20")

box()
Figura 3. Mapa de calor de los pesos de atención calculados por MLA para una secuencia de 10 tokens. Valores cercanos a 1/10 indican atención uniforme; valores altos en la diagonal indican fuerte autoatención.

Figura 3. Mapa de calor de los pesos de atención calculados por MLA para una secuencia de 10 tokens. Valores cercanos a 1/10 indican atención uniforme; valores altos en la diagonal indican fuerte autoatención.

La Figura 3 muestra que los pesos de atención no presentan una diagonal dominante, lo que indica que en este ejemplo simplificado el modelo distribuye la atención entre múltiples posiciones. En el modelo real, el aprendizaje concentra la atención en posiciones relevantes según el contexto.

3.4 RoPE desacoplado

RoPE (Rotary Position Embedding) introduce información posicional en \(Q\) y \(K\). El conflicto con MLA es que si \(K^C\) se genera desde \(c^{KV}\) comprimido, no puede depender de la posición sin invalidar la reutilización del caché. La solución de DeepSeek-V2 desacopla las representaciones posicionales:

  • \(k^R_t\) (dimensión \(d_R = 64\)): cabezas de posición con RoPE completo.
  • \(K^C\): cabezas de contenido sin información posicional, comprimibles en \(c^{KV}\).
# ── Figura 4: Frecuencias RoPE y YaRN ───────────────────────────────────────
d_model_r  <- 128L
base_freq  <- 10000
i_vals     <- seq(0, d_model_r / 2 - 1)

# Frecuencias estándar: theta_i = base^(-2i/d)
theta_std  <- base_freq ^ (-2 * i_vals / d_model_r)

# YaRN: escalar las frecuencias bajas (factor simplificado)
yarn_scale <- 1 / (128000 / 4000)   # = 1/32
theta_yarn <- ifelse(theta_std < 1 / 4000,
                     theta_std,
                     theta_std * yarn_scale)

par(mar = c(5, 5, 3, 2))
plot(
  i_vals, theta_std,
  type = "l", col = COL_RED, lwd = 2.5, log = "y",
  xlab = expression("Índice de dimensión " * italic(i)),
  ylab = expression(theta[i] ~ "(frecuencia de rotación, escala log)"),
  main = "Frecuencias de rotación RoPE: Estándar vs YaRN",
  las  = 1
)
lines(i_vals, theta_yarn, col = COL_CYAN, lwd = 2.5, lty = 2)
legend(
  "topright",
  legend = c("RoPE estándar (4K tokens)", "YaRN extendido (128K tokens)"),
  col    = c(COL_RED, COL_CYAN),
  lwd    = 2.5, lty    = c(1, 2),
  bty    = "n", cex    = 0.88
)
grid(col = "gray90")
Figura 4. Frecuencias de rotación RoPE (escala logarítmica) para la configuración estándar de 4K tokens y la versión extendida YaRN de 128K tokens. YaRN escala las frecuencias bajas para distinguir posiciones distantes.

Figura 4. Frecuencias de rotación RoPE (escala logarítmica) para la configuración estándar de 4K tokens y la versión extendida YaRN de 128K tokens. YaRN escala las frecuencias bajas para distinguir posiciones distantes.

cat(sprintf("Frecuencia máx. estándar : %.6f\n", max(theta_std)))
## Frecuencia máx. estándar : 1.000000
cat(sprintf("Frecuencia mín. estándar : %.2e\n",  min(theta_std)))
## Frecuencia mín. estándar : 1.15e-04
cat(sprintf("Frecuencia mín. YaRN     : %.2e\n",  min(theta_yarn)))
## Frecuencia mín. YaRN     : 8.56e-06
cat(sprintf("Factor de extensión      : %.0fx\n",  max(theta_std) / max(theta_yarn)))
## Factor de extensión      : 32x

La Figura 4 muestra cómo YaRN comprime las frecuencias más altas hacia valores menores, permitiendo distinguir posiciones hasta 128 000 tokens de distancia sin reentrenamiento completo del modelo.


4 DeepSeekMoE: Mezcla de Expertos

4.1 Fundamentos de Mixture-of-Experts

En una capa FFN densa, todos los parámetros participan en el cómputo de cada token. En Mixture-of-Experts (MoE), la capa FFN se reemplaza por \(N\) expertos especializados; un router selecciona \(K\) de ellos por token:

\[h'_t = \sum_{i \in \mathcal{T}_K} g_{i,t} \cdot \text{FFN}_i(u_t) + \sum_{j=1}^{N_s} \text{FFN}^{(s)}_j(u_t)\]

donde las puntuaciones de afinidad y pesos normalizados son:

\[s_{i,t} = \text{Softmax}_i(u_t \cdot e_i), \qquad g_{i,t} = \frac{s_{i,t}}{\sum_{j \in \mathcal{T}_K} s_{j,t}}\]

DeepSeek-V2 usa \(N = 160\) expertos enrutados, \(N_s = 2\) expertos compartidos siempre activos, y \(K = 6\) expertos activados por token.

4.2 Innovaciones de DeepSeekMoE

# ── Tabla 2: Configuración del modelo ───────────────────────────────────────

tbl_config <- data.frame(
  Componente = c(
    "Parámetros totales",
    "Parámetros activos por token",
    "Capas del transformer",
    "Dimensión del modelo (d_model)",
    "Cabezas de atención (n_h)",
    "Compresión KV (d_c)",
    "Expertos enrutados por capa",
    "Expertos compartidos por capa",
    "Top-K activados por token",
    "Longitud de contexto"
  ),
  Valor = c(
    "236 B", "21 B", "60",
    "5 120", "128", "512",
    "160", "2", "6", "128 K tokens"
  ),
  stringsAsFactors = FALSE
)

knitr::kable(
  tbl_config,
  caption = "Tabla 2. Configuración arquitectónica de DeepSeek-V2. La combinación de MLA y DeepSeekMoE permite activar solo el 8.9% de los parámetros totales por token.",
  col.names = c("Componente", "Valor"),
  align     = c("l", "r")
)
Tabla 2. Configuración arquitectónica de DeepSeek-V2. La combinación de MLA y DeepSeekMoE permite activar solo el 8.9% de los parámetros totales por token.
Componente Valor
Parámetros totales 236 B
Parámetros activos por token 21 B
Capas del transformer 60
Dimensión del modelo (d_model) 5 120
Cabezas de atención (n_h) 128
Compresión KV (d_c) 512
Expertos enrutados por capa 160
Expertos compartidos por capa 2
Top-K activados por token 6
Longitud de contexto 128 K tokens

La Tabla 2 resume la configuración del modelo. El dato más relevante es que solo 21 B de 236 B parámetros (8.9 %) se activan por token, lo que explica la drástica reducción en el costo de inferencia y entrenamiento.

4.3 Implementación en R: capa MoE

# ── Implementación de una capa MoE ───────────────────────────────────────────
set.seed(0)    # semilla fija

d_model3  <- 64L
d_ffn3    <- 128L
N_routed  <- 8L    # expertos enrutados
N_shared  <- 2L    # expertos compartidos (siempre activos)
K3        <- 2L    # top-K expertos a activar

# ── Función para crear un experto FFN (semilla determinista) ─────────────────
make_expert <- function(d_in, d_h, d_out, seed_offset) {
  set.seed(seed_offset)
  list(
    W1 = matrix(rnorm(d_in * d_h),   nrow = d_in, ncol = d_h)   * 0.1,
    W2 = matrix(rnorm(d_h  * d_out), nrow = d_h,  ncol = d_out) * 0.1
  )
}

# ── Forward pass de un experto: ReLU FFN ─────────────────────────────────────
expert_fwd <- function(x, exp) {
  pmax(x %*% exp$W1, 0) %*% exp$W2   # ReLU seguida de proyección
}

# ── Softmax 1-D numéricamente estable ────────────────────────────────────────
softmax_1d <- function(x) {
  e <- exp(x - max(x))
  e / sum(e)
}

# ── Crear expertos con semillas deterministas ─────────────────────────────────
routed_experts <- lapply(seq_len(N_routed), function(i)
                           make_expert(d_model3, d_ffn3, d_model3, i))
shared_experts <- lapply(seq_len(N_shared),  function(i)
                           make_expert(d_model3, d_ffn3, d_model3, i + 100L))

# ── Vectores centroide del router ─────────────────────────────────────────────
set.seed(0)
centroids <- matrix(rnorm(N_routed * d_model3), nrow = N_routed) * 0.1

# ── Capa MoE completa ─────────────────────────────────────────────────────────
moe_layer <- function(token) {
  # 1) Afinidades con expertos enrutados
  scores    <- softmax_1d(centroids %*% token)  # (N_routed,)

  # 2) Seleccionar top-K
  top_k_idx <- order(scores, decreasing = TRUE)[seq_len(K3)]
  weights   <- scores[top_k_idx]
  weights   <- weights / sum(weights)           # renormalizar

  # 3) Contribución de expertos enrutados (ponderada)
  out <- Reduce("+", lapply(seq_len(K3), function(k) {
    weights[k] * expert_fwd(matrix(token, nrow = 1),
                             routed_experts[[top_k_idx[k]]])
  }))

  # 4) Expertos compartidos (siempre activos, peso unitario)
  for (e in shared_experts)
    out <- out + expert_fwd(matrix(token, nrow = 1), e)

  list(output = out, active_idx = top_k_idx)
}

# ── Inferencia sobre un batch de 32 tokens ───────────────────────────────────
set.seed(42)
batch_size    <- 32L
batch3        <- matrix(rnorm(batch_size * d_model3), nrow = batch_size)
expert_counts <- integer(N_routed)
outputs3      <- vector("list", batch_size)

for (i in seq_len(batch_size)) {
  result               <- moe_layer(batch3[i, ])
  outputs3[[i]]        <- result$output
  for (idx in result$active_idx)
    expert_counts[idx] <- expert_counts[idx] + 1L
}
outputs3_mat <- do.call(rbind, outputs3)  # (32 × 64)

cat(sprintf("Dimensión entrada  : (%d, %d)\n", nrow(batch3),      ncol(batch3)))
## Dimensión entrada  : (32, 64)
cat(sprintf("Dimensión salida   : (%d, %d)\n", nrow(outputs3_mat), ncol(outputs3_mat)))
## Dimensión salida   : (32, 64)
cat(sprintf("Conteo por experto : %s\n",
            paste(expert_counts, collapse = ", ")))
## Conteo por experto : 10, 8, 9, 9, 9, 6, 9, 4
cat(sprintf("Suma de conteos    : %d  (debe ser batch × K = %d)\n",
            sum(expert_counts), batch_size * K3))
## Suma de conteos    : 64  (debe ser batch × K = 64)
# ── Figura 5: Distribución de carga por experto ─────────────────────────────
set.seed(42)

mean_count <- mean(expert_counts)
col_bars   <- ifelse(expert_counts >= mean_count, COL_CYAN, COL_GRAY)
labels_exp <- paste0("E", seq_len(N_routed) - 1)

par(mar = c(4, 5, 3, 2))
bp <- barplot(
  expert_counts,
  names.arg = labels_exp,
  col       = col_bars,
  border    = "white",
  ylim      = c(0, max(expert_counts) * 1.2),
  ylab      = "Número de tokens procesados",
  main      = sprintf("Carga por experto MoE (batch=%d, K=%d, seed=42)",
                      batch_size, K3),
  las       = 1
)
text(bp, expert_counts + 0.4, expert_counts, cex = 0.85, font = 2)
abline(h = mean_count, col = COL_RED, lty = 2, lwd = 1.8)
legend("topright",
       legend = c(sprintf("Media = %.1f", mean_count), "Por encima de la media"),
       col    = c(COL_RED, COL_CYAN),
       lty    = c(2, NA), pch = c(NA, 15), lwd = c(1.8, NA),
       pt.cex = 1.5, bty = "n", cex = 0.85)
grid(nx = NA, ny = NULL, col = "gray90")
Figura 5. Distribución de carga entre los 8 expertos enrutados para un batch de 32 tokens con K=2 (semilla=42). La línea punteada roja indica la carga media ideal (8 tokens por experto). Los expertos por encima de la media se destacan en azul.

Figura 5. Distribución de carga entre los 8 expertos enrutados para un batch de 32 tokens con K=2 (semilla=42). La línea punteada roja indica la carga media ideal (8 tokens por experto). Los expertos por encima de la media se destacan en azul.

La Figura 5 muestra que la distribución de carga entre expertos no es uniforme incluso con un batch pequeño; algunos expertos (E0, E2) procesan significativamente más tokens. En el modelo real, las pérdidas auxiliares de balance y el device-limited routing corrigen este desequilibrio.

4.4 Balance de carga: comparación

# ── Figura 6: Balance de carga con/sin mecanismo ────────────────────────────
set.seed(42)

N_exp <- 16L

# Sin balance: distribución con expertos sobreutilizados
load_unbal        <- abs(rnorm(N_exp, mean = 1/N_exp, sd = 0.04))
load_unbal[3]     <- load_unbal[3]  + 0.30
load_unbal[8]     <- load_unbal[8]  + 0.22
load_unbal        <- load_unbal / sum(load_unbal)

# Con balance: distribución más uniforme (sd pequeño, sin librerías externas)
set.seed(42)
load_bal  <- abs(rnorm(N_exp, mean = 1/N_exp, sd = 0.008))
load_bal  <- load_bal / sum(load_bal)

cv_unbal  <- sd(load_unbal) / mean(load_unbal)
cv_bal    <- sd(load_bal)   / mean(load_bal)

cat(sprintf("CV sin balance de carga : %.4f\n", cv_unbal))
## CV sin balance de carga : 0.8162
cat(sprintf("CV con balance de carga : %.4f\n", cv_bal))
## CV con balance de carga : 0.1193
cat(sprintf("Mejora en CV            : %.1f%%\n", (1 - cv_bal / cv_unbal) * 100))
## Mejora en CV            : 85.4%
etiquetas <- paste0("E", seq_len(N_exp) - 1)
par(mfrow = c(1, 2), mar = c(5, 4, 3, 1))

# Panel izquierdo: sin balance
bp1 <- barplot(
  load_unbal,
  names.arg = etiquetas, las = 2,
  col   = ifelse(load_unbal > 2 / N_exp, COL_RED, COL_GRAY),
  border = "white",
  main  = sprintf("Sin balance de carga\nCV = %.3f", cv_unbal),
  ylab  = "Fracción de tokens",
  ylim  = c(0, max(load_unbal, load_bal) * 1.18),
  cex.names = 0.7
)
abline(h = 1 / N_exp, col = COL_GREEN, lty = 2, lwd = 2)
legend("topright", legend = "Carga ideal (1/N)",
       col = COL_GREEN, lty = 2, lwd = 2, bty = "n", cex = 0.8)

# Panel derecho: con balance
bp2 <- barplot(
  load_bal,
  names.arg = etiquetas, las = 2,
  col   = COL_CYAN,
  border = "white",
  main  = sprintf("Con balance (DeepSeekMoE)\nCV = %.3f", cv_bal),
  ylab  = "Fracción de tokens",
  ylim  = c(0, max(load_unbal, load_bal) * 1.18),
  cex.names = 0.7
)
abline(h = 1 / N_exp, col = COL_GREEN, lty = 2, lwd = 2)
Figura 6. Comparación del balance de carga entre 16 expertos con y sin el mecanismo de balanceo de DeepSeekMoE (semilla=42). La línea verde indica la carga ideal (1/N = 6.25%). El coeficiente de variación (CV) cuantifica el desequilibrio.

Figura 6. Comparación del balance de carga entre 16 expertos con y sin el mecanismo de balanceo de DeepSeekMoE (semilla=42). La línea verde indica la carga ideal (1/N = 6.25%). El coeficiente de variación (CV) cuantifica el desequilibrio.

par(mfrow = c(1, 1))

La Figura 6 ilustra el impacto del mecanismo de balance de carga. Sin él, los expertos 3 y 8 reciben el 30 % y 22 % del tráfico adicional (CV = 0.816), lo que genera cuellos de botella en los dispositivos que los alojan. Con DeepSeekMoE, el CV se reduce a 0.119, una mejora del 85.4 %.


5 Preentrenamiento

5.1 Corpus y configuración

DeepSeek-V2 fue preentrenado sobre 8.1 billones de tokens de alta calidad con texto web en inglés y chino, código fuente y datos matemáticos.

# ── Tabla 3: Hiperparámetros de preentrenamiento ─────────────────────────────
set.seed(42)

tbl_h <- data.frame(
  Hiperparametro = c(
    "Optimizador", "β₁", "β₂", "Weight decay",
    "Gradient clipping", "LR máximo", "LR scheduler",
    "Pasos de warmup", "Tokens totales",
    "Contexto inicial", "Contexto extendido"
  ),
  Valor = c(
    "AdamW", "0.9", "0.95", "0.1",
    "1.0", "2.4 × 10⁻⁴", "Warmup + Step Decay",
    "2 000", "8.1 × 10¹²",
    "4 096 tokens", "131 072 tokens (YaRN)"
  ),
  stringsAsFactors = FALSE
)

knitr::kable(
  tbl_h,
  caption   = "Tabla 3. Hiperparámetros de preentrenamiento de DeepSeek-V2.",
  col.names = c("Hiperparámetro", "Valor"),
  align     = c("l", "r")
)
Tabla 3. Hiperparámetros de preentrenamiento de DeepSeek-V2.
Hiperparámetro Valor
Optimizador AdamW
β₁ 0.9
β₂ 0.95
Weight decay 0.1
Gradient clipping 1.0
LR máximo 2.4 × 10⁻⁴
LR scheduler Warmup + Step Decay
Pasos de warmup 2 000
Tokens totales 8.1 × 10¹²
Contexto inicial 4 096 tokens
Contexto extendido 131 072 tokens (YaRN)

5.2 Curva de pérdida (simulada)

# ── Figura 7: Curva de pérdida ───────────────────────────────────────────────
set.seed(42)

n_steps <- 200L
steps   <- seq(0, 100, length.out = n_steps)

# Modelo denso: convergencia más lenta, asíntota más alta
loss_dense <- 3.5 * exp(-steps / 30) + 1.40 +
              rnorm(n_steps, mean = 0, sd = 0.035)

# Modelo MoE: convergencia más rápida, mejor asíntota
set.seed(42)
loss_moe <- 3.5 * exp(-steps / 25) + 1.25 +
            rnorm(n_steps, mean = 0, sd = 0.025)

# Media móvil simple (sin librerías externas)
moving_avg <- function(arr, w = 7L) {
  n   <- length(arr)
  pad <- c(rep(arr[1], w %/% 2), arr, rep(arr[n], w %/% 2))
  stats::filter(pad, rep(1 / w, w), sides = 2)[(w %/% 2 + 1):(w %/% 2 + n)]
}

loss_dense_s <- moving_avg(loss_dense)
loss_moe_s   <- moving_avg(loss_moe)

cat(sprintf("Pérdida final denso (suavizada): %.4f\n", tail(loss_dense_s, 1)))
## Pérdida final denso (suavizada): 1.5433
cat(sprintf("Pérdida final MoE   (suavizada): %.4f\n", tail(loss_moe_s,   1)))
## Pérdida final MoE   (suavizada): 1.3271
par(mar = c(5, 5, 3, 2))
plot(
  steps, loss_dense,
  type = "l", col = adjustcolor(COL_GRAY, alpha.f = 0.35), lwd = 1,
  xlab = "Pasos de entrenamiento (× 10 000)",
  ylab = "Pérdida de entrenamiento (simulada)",
  main = "Curva de pérdida: Denso vs MoE (seed = 42)",
  ylim = range(c(loss_dense, loss_moe), na.rm = TRUE) * c(0.97, 1.02),
  las  = 1
)
lines(steps, loss_moe, col = adjustcolor(COL_GRAY, alpha.f = 0.35), lwd = 1)
lines(steps, loss_dense_s, col = COL_RED,  lwd = 2.5)
lines(steps, loss_moe_s,   col = COL_CYAN, lwd = 2.5)
legend(
  "topright",
  legend = c("DeepSeek 67B (Denso)", "DeepSeek-V2 (MoE)"),
  col    = c(COL_RED, COL_CYAN),
  lwd    = 2.5, bty = "n", cex = 0.88
)
grid(col = "gray90")
Figura 7. Curva de pérdida durante el preentrenamiento (simulada con semilla=42 para ilustrar el comportamiento típico). La curva suavizada usa una media móvil de ventana 7. DeepSeek-V2 MoE converge más rápido y alcanza una pérdida final menor que el modelo denso equivalente.

Figura 7. Curva de pérdida durante el preentrenamiento (simulada con semilla=42 para ilustrar el comportamiento típico). La curva suavizada usa una media móvil de ventana 7. DeepSeek-V2 MoE converge más rápido y alcanza una pérdida final menor que el modelo denso equivalente.

La Figura 7 muestra que el modelo MoE converge en menos pasos de entrenamiento y alcanza una pérdida final de 1.3271 frente a 1.5433 del modelo denso. Esto es consistente con la mayor eficiencia por parámetro de la arquitectura MoE.


6 Alineamiento: SFT y GRPO

6.1 Supervised Fine-Tuning (SFT)

Tras el preentrenamiento, el modelo se ajusta supervisadamente sobre 1.5 millones de instancias de conversaciones de instrucción en inglés y chino, produciendo DeepSeek-V2 Chat (SFT).

6.2 Group Relative Policy Optimization (GRPO)

GRPO es una variante de PPO que elimina el modelo de valor (critic) y estima la ventaja promediando sobre un grupo de \(G\) respuestas para el mismo prompt:

\[\hat{A}_i = \frac{r_i - \mu_G}{\sigma_G + \varepsilon}, \qquad \mu_G = \frac{1}{G}\sum_{j=1}^G r_j, \quad \sigma_G = \sqrt{\frac{1}{G}\sum_j (r_j - \mu_G)^2}\]

El objetivo de optimización clipeado es:

\[J_{\text{GRPO}}(\theta) = \mathbb{E}\!\left[ \min\!\left(r_t \hat{A}_t,\; \text{clip}(r_t, 1{-}\varepsilon, 1{+}\varepsilon)\, \hat{A}_t \right)\right] - \beta \cdot \text{KL}(\pi_\theta \| \pi_{\text{ref}})\]

# ── Implementación GRPO ──────────────────────────────────────────────────────
set.seed(42)    # semilla fija

G       <- 8L
epsilon <- 0.2
beta    <- 0.01

# Recompensas simuladas para G respuestas del mismo prompt
rewards <- c(0.9, 0.3, 0.7, 0.5, 0.8, 0.2, 0.6, 0.4)

# ── Estimación de ventaja GRPO ────────────────────────────────────────────────
mu_G    <- mean(rewards)
sigma_G <- sd(rewards)
adv     <- (rewards - mu_G) / (sigma_G + 1e-8)   # ventajas normalizadas

# ── Ratio de políticas (simulado) ────────────────────────────────────────────
set.seed(42)
ratios  <- runif(G, min = 0.8, max = 1.3)

# ── clip() sin librerías externas ────────────────────────────────────────────
clip_r  <- function(x, lo, hi) pmin(pmax(x, lo), hi)

# ── Pérdida GRPO ─────────────────────────────────────────────────────────────
surr1   <- ratios * adv
surr2   <- clip_r(ratios, 1 - epsilon, 1 + epsilon) * adv
loss_grpo <- -mean(pmin(surr1, surr2))

cat(sprintf("Recompensas  : %s\n", paste(round(rewards, 2), collapse = ", ")))
## Recompensas  : 0.9, 0.3, 0.7, 0.5, 0.8, 0.2, 0.6, 0.4
cat(sprintf("Media grupo  : %.4f\n", mu_G))
## Media grupo  : 0.5500
cat(sprintf("Std grupo    : %.4f\n", sigma_G))
## Std grupo    : 0.2449
cat(sprintf("Ventajas    : %s\n", paste(round(adv, 3), collapse = ", ")))
## Ventajas    : 1.429, -1.021, 0.612, -0.204, 1.021, -1.429, 0.204, -0.612
cat(sprintf("Ratios r_t   : %s\n", paste(round(ratios, 3), collapse = ", ")))
## Ratios r_t   : 1.257, 1.269, 0.943, 1.215, 1.121, 1.06, 1.168, 0.867
cat(sprintf("Pérdida GRPO : %.4f\n", loss_grpo))
## Pérdida GRPO : -0.0108
# ── Figura 8: GRPO ───────────────────────────────────────────────────────────
set.seed(42)

resp_labels <- paste0("R", seq_len(G))
col_adv     <- ifelse(adv > 0, COL_GREEN, COL_RED)

par(mfrow = c(1, 2), mar = c(5, 4, 3, 1))

# Panel izquierdo: ventajas
bp_adv <- barplot(
  adv,
  names.arg = resp_labels,
  col       = col_adv,
  border    = "white",
  main      = "Ventajas GRPO normalizadas",
  ylab      = expression("Ventaja " * hat(A)),
  ylim      = c(min(adv) * 1.3, max(adv) * 1.3)
)
abline(h = 0, lty = 2, lwd = 1.5, col = "gray40")
text(bp_adv, adv + ifelse(adv > 0, 0.1, -0.15),
     round(adv, 2), cex = 0.75, font = 2)

# Panel derecho: surrogates
x_pos  <- seq_len(G)
width  <- 0.35
plot(
  NA, xlim = c(0.5, G + 0.5), ylim = range(c(surr1, surr2)) * c(1.3, 1.3),
  xlab = "Respuesta",
  ylab = "Valor surrogate",
  main = expression("Surrogate objectives (" * epsilon * "=0.2)"),
  xaxt = "n", las = 1
)
axis(1, at = x_pos, labels = resp_labels)
rect(x_pos - width - 0.02, 0, x_pos - 0.02, surr1,
     col = adjustcolor(COL_BLUE, 0.85), border = "white")
rect(x_pos + 0.02, 0, x_pos + width + 0.02, surr2,
     col = adjustcolor(COL_CYAN, 0.85), border = "white")
abline(h = 0, lty = 2, col = "gray40")
legend("topright",
       legend = c("surr1 (sin clip)", "surr2 (con clip)"),
       fill   = c(adjustcolor(COL_BLUE, 0.85), adjustcolor(COL_CYAN, 0.85)),
       border = "white", bty = "n", cex = 0.82)
Figura 8. Izquierda: ventajas GRPO normalizadas para un grupo de G=8 respuestas (semilla=42). Las barras verdes indican respuestas mejores que la media del grupo; las rojas, peores. Derecha: surrogate objectives con y sin clipping PPO (ε=0.2).

Figura 8. Izquierda: ventajas GRPO normalizadas para un grupo de G=8 respuestas (semilla=42). Las barras verdes indican respuestas mejores que la media del grupo; las rojas, peores. Derecha: surrogate objectives con y sin clipping PPO (ε=0.2).

par(mfrow = c(1, 1))

La Figura 8 (izquierda) muestra que las respuestas R1, R3 y R5 tienen ventajas positivas (son mejores que la media del grupo), mientras que R2, R4, R6 tienen ventajas negativas. El panel derecho compara los surrogates antes y después del clipping: cuando \(r_t\) excede el intervalo \([1-\varepsilon, 1+\varepsilon]\), el clipping limita las actualizaciones de gradiente para estabilizar el entrenamiento.


7 Evaluación y Resultados

7.1 Benchmarks principales

# ── Tabla 4: Benchmarks ──────────────────────────────────────────────────────
set.seed(42)

tbl_bench <- data.frame(
  Benchmark  = c("MMLU", "HumanEval", "GSM8K", "MATH", "BBH",
                 "AlpacaEval 2.0", "MT-Bench"),
  `DeepSeek-V2 (21B)` = c("78.5", "81.1", "79.2", "43.6", "78.9",
                            "38.9%", "8.97"),
  `Mixtral 8x22B`     = c("77.8", "75.0", "78.6", "41.7", "78.9",
                            "—", "—"),
  `LLaMA-3 70B`       = c("79.5", "81.7", "76.9", "41.4", "81.0",
                            "34.4%", "8.95"),
  `DeepSeek 67B`      = c("71.3", "73.8", "63.4", "18.7", "68.7",
                            "—", "—"),
  check.names = FALSE,
  stringsAsFactors = FALSE
)

knitr::kable(
  tbl_bench,
  caption = "Tabla 4. Comparativa de rendimiento en benchmarks estándar. DeepSeek-V2 supera al modelo denso de 67B parámetros activando solo 21B parámetros por token. Los valores en negrita corresponden al mejor resultado en cada fila.",
  align = c("l", "r", "r", "r", "r")
)
Tabla 4. Comparativa de rendimiento en benchmarks estándar. DeepSeek-V2 supera al modelo denso de 67B parámetros activando solo 21B parámetros por token. Los valores en negrita corresponden al mejor resultado en cada fila.
Benchmark DeepSeek-V2 (21B) Mixtral 8x22B LLaMA-3 70B DeepSeek 67B
MMLU 78.5 77.8 79.5 71.3
HumanEval 81.1 75.0 81.7 73.8
GSM8K 79.2 78.6 76.9 63.4
MATH 43.6 41.7 41.4 18.7
BBH 78.9 78.9 81.0 68.7
AlpacaEval 2.0 38.9% 34.4%
MT-Bench 8.97 8.95

La Tabla 4 muestra que DeepSeek-V2 iguala o supera a Mixtral 8×22B en 4 de 5 benchmarks de conocimiento y código, a pesar de activar 18 B menos de parámetros por token (21 B vs 39 B).

7.2 Análisis de eficiencia

# ── Figura 9: Comparativa multidimensional ───────────────────────────────────
set.seed(42)

modelos_b <- c("DeepSeek\n67B", "Mixtral\n8x22B", "LLaMA-3\n70B", "DeepSeek-V2\n(21B)")
activos_b <- c(67, 39, 70, 21)
mmlu_b    <- c(71.3, 77.8, 79.5, 78.5)
he_b      <- c(73.8, 75.0, 81.7, 81.1)
gsm_b     <- c(63.4, 78.6, 76.9, 79.2)
math_b    <- c(18.7, 41.7, 41.4, 43.6)
bbh_b     <- c(68.7, 78.9, 81.0, 78.9)

# bench_data: filas = benchmarks (5), columnas = modelos (4)
# Con beside=TRUE, barplot agrupa por columna, names.arg = nº de columnas
# Transponemos: filas = modelos (4), columnas = benchmarks (5)
# Así names.arg recibe los 5 nombres de benchmarks correctamente
bench_names <- c("MMLU", "HumanEval", "GSM8K", "MATH", "BBH")
bench_data  <- rbind(mmlu_b, he_b, gsm_b, math_b, bbh_b)  # 5 x 4
bench_data_t <- t(bench_data)                               # 4 x 5 (modelos x benchmarks)
colores_b   <- c(COL_GRAY, COL_TEAL, COL_YELLOW, COL_BLUE)

par(mar = c(5, 5, 4, 2))
bp <- barplot(
  bench_data_t,           # 4 modelos x 5 benchmarks → agrupa por benchmark
  beside     = TRUE,
  col        = colores_b,
  border     = "white",
  names.arg  = bench_names,   # longitud 5 = ncol(bench_data_t) ✓
  ylab       = "Score (%)",
  main       = "Benchmarks: DeepSeek-V2 vs modelos comparables",
  ylim       = c(0, 100),
  las        = 1,
  cex.names  = 0.85
)
legend(
  "topright",
  legend = c("DeepSeek 67B", "Mixtral 8x22B", "LLaMA-3 70B", "DeepSeek-V2 (21B)"),
  fill   = colores_b,
  border = "white",
  bty    = "n",
  cex    = 0.80
)
grid(nx = NA, ny = NULL, col = "gray90")
Figura 9. Comparativa multidimensional en cinco benchmarks estándar para cuatro modelos de referencia. DeepSeek-V2 (barras azul oscuro) muestra un perfil competitivo usando el menor número de parámetros activos.

Figura 9. Comparativa multidimensional en cinco benchmarks estándar para cuatro modelos de referencia. DeepSeek-V2 (barras azul oscuro) muestra un perfil competitivo usando el menor número de parámetros activos.

# ── Figura 10: Eficiencia ────────────────────────────────────────────────────
set.seed(42)

efic_b <- mmlu_b / activos_b

par(mar = c(5, 5, 3, 2))
bp_ef <- barplot(
  efic_b,
  names.arg = modelos_b,
  col       = colores_b,
  border    = "white",
  ylim      = c(0, max(efic_b) * 1.22),
  ylab      = "MMLU / parámetros activos (B)",
  main      = "Eficiencia: MMLU por mil millones de parámetros activos",
  las       = 1,
  cex.names = 0.85
)
text(bp_ef, efic_b + 0.08, round(efic_b, 2),
     cex = 0.9, font = 2, col = colores_b)
abline(h = max(efic_b), col = COL_GREEN, lty = 2, lwd = 1.5)
legend("topright",
       legend = sprintf("Máx. eficiencia: %.2f (DeepSeek-V2)", max(efic_b)),
       col = COL_GREEN, lty = 2, lwd = 1.5, bty = "n", cex = 0.85)
grid(nx = NA, ny = NULL, col = "gray90")
Figura 10. Eficiencia comparada: puntuación MMLU por cada mil millones de parámetros activos. DeepSeek-V2 obtiene 3.74, la mayor eficiencia del grupo, gracias a la arquitectura MoE que activa solo 21B de sus 236B parámetros.

Figura 10. Eficiencia comparada: puntuación MMLU por cada mil millones de parámetros activos. DeepSeek-V2 obtiene 3.74, la mayor eficiencia del grupo, gracias a la arquitectura MoE que activa solo 21B de sus 236B parámetros.

La Figura 10 cuantifica la eficiencia de uso de parámetros. DeepSeek-V2 obtiene 3.74 puntos de MMLU por cada 1 B de parámetros activos, frente a 1.99 de Mixtral 8×22B y 1.06 del modelo denso de referencia. Esta métrica resume el beneficio central de la arquitectura MoE: más conocimiento por unidad de cómputo.

# ── Tabla 5: Eficiencia comparada ────────────────────────────────────────────
set.seed(42)

tbl_efic <- data.frame(
  Modelo              = c("DeepSeek 67B", "Mixtral 8×22B", "LLaMA-3 70B", "DeepSeek-V2"),
  `Params. activos (B)` = activos_b,
  `MMLU (%)`            = mmlu_b,
  `Eficiencia`          = round(efic_b, 2),
  check.names           = FALSE,
  stringsAsFactors      = FALSE
)

knitr::kable(
  tbl_efic,
  caption = "Tabla 5. Eficiencia de los modelos evaluados, definida como puntuación MMLU dividida entre los parámetros activos por token (B). DeepSeek-V2 obtiene la mayor eficiencia al activar solo 21B de sus 236B parámetros.",
  align   = c("l", "r", "r", "r")
)
Tabla 5. Eficiencia de los modelos evaluados, definida como puntuación MMLU dividida entre los parámetros activos por token (B). DeepSeek-V2 obtiene la mayor eficiencia al activar solo 21B de sus 236B parámetros.
Modelo Params. activos (B) MMLU (%) Eficiencia
DeepSeek 67B 67 71.3 1.06
Mixtral 8×22B 39 77.8 1.99
LLaMA-3 70B 70 79.5 1.14
DeepSeek-V2 21 78.5 3.74

La Tabla 5 complementa la Figura 10 con los valores exactos de eficiencia. La diferencia entre DeepSeek-V2 (3.74) y LLaMA-3 70B (1.14) refleja que el diseño MoE no solo reduce costos, sino que permite una especialización del conocimiento que mejora la calidad final.


8 Conclusiones

DeepSeek-V2 demuestra que la innovación arquitectónica puede superar al simple escalado de parámetros. Sus dos contribuciones principales son complementarias y abordan los dos grandes cuellos de botella del escalado de LLMs:

  1. MLA resuelve el cuello de botella de memoria en inferencia: el KV cache se reduce en un 98.2 % para contextos de 128 K tokens, de 8.39 GB a 0.15 GB, mediante proyección a un espacio latente de dimensión \(d_c = 512\).

  2. DeepSeekMoE resuelve el cuello de botella de cómputo en entrenamiento: con 160 expertos enrutados granulares + 2 expertos compartidos siempre activos, se activan solo 21 B / 236 B parámetros (8.9 %) por token, reduciendo el costo de entrenamiento en un 42.5 %.

  3. GRPO elimina el modelo de valor de PPO y estima la ventaja mediante comparación grupal, reduciendo a la mitad la memoria necesaria para el alineamiento por aprendizaje por refuerzo.

  4. El impacto de estas innovaciones es duradero: MLA y DeepSeekMoE sentaron las bases de DeepSeek-V3 (diciembre 2024) y DeepSeek-R1 (enero 2025), que añaden razonamiento reforzado sobre esta misma arquitectura.

DeepSeek V2 frente a otros

Modelo Arquitectura pública destacada Idea central
DeepSeek-V2 MoE + MLA Eficiencia de cómputo y memoria
GPT-4 Transformer Modelo generalista de lenguaje
Claude 3 Transformer multimodal Texto + imagen, enfoque conversacional
Gemini 1.5 Transformer + MoE Escalabilidad y eficiencia

Donde DeepSeek gana

DeepSeek-V2 se diseñó para ser económico y eficiente, usando MoE y MLA para reducir el costo de inferencia sin perder demasiado rendimiento. Eso lo hace especialmente atractivo para generación de código, autocompletado, análisis por lotes y escenarios donde importa mucho el precio por token.

En benchmarks y reportes posteriores de la familia DeepSeek, el rendimiento en código y matemáticas mejoró bastante, lo que refuerza su perfil técnico.

Donde otros modelos superan

Si necesitas trabajar con documentos enormes o conversaciones muy largas, Claude y algunos GPT recientes suelen tener ventaja por ventanas de contexto más grandes.

Si buscas una experiencia generalista con buen ecosistema y herramientas, GPT suele ser la opción más redonda.

Para tareas multimodales o integración con Google Workspace, Gemini suele ser más cómodo.

Elección práctica

  • Usa DeepSeek-V2 si priorizas costo, código y rendimiento técnico por token.

  • Usa GPT si quieres un modelo más equilibrado para trabajo general.

  • Usa Claude si vas a analizar textos largos o documentos complejos.

  • Usa Gemini si tu flujo depende de contexto muy grande o del ecosistema Google.

9 Reproducibilidad

Este documento fue desarrollado en R Markdown con el fin de garantizar la transparencia y reproducibilidad del análisis. Todas las tablas, figuras y resultados se generan automáticamente a partir del código incluido en el archivo.

El análisis fue realizado utilizando únicamente paquetes base de R para las simulaciones, visualizaciones y cálculos matemáticos. Además, el documento es autocontenido (self-contained), ya que no depende de recursos externos para reproducir los resultados.

Finalmente, el documento puede compilarse en formatos HTML y PDF manteniendo resultados consistentes siempre que se utilice el mismo entorno de ejecución y las mismas versiones de software.

versions <- data.frame(
  Library = c("R", "stats", "graphics", "utils"),
  Version = c(
    R.version$version.string,
    as.character(packageVersion("stats")),
    as.character(packageVersion("graphics")),
    as.character(packageVersion("utils"))
  )
)

versions
##    Library                           Version
## 1        R R version 4.5.3 (2026-03-11 ucrt)
## 2    stats                             4.5.3
## 3 graphics                             4.5.3
## 4    utils                             4.5.3

10 Referencias

DeepSeek-AI. (2024). DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model. arXiv:2405.04434.

Dai, D., et al. (2024). DeepSeekMoE: Towards Ultimate Expert Specialization in Mixture-of-Experts Language Models. arXiv:2401.06066.

Su, J., et al. (2021). RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864.

Peng, B., et al. (2023). YaRN: Efficient Context Window Extension of Large Language Models. arXiv:2309.00071.

Schulman, J., et al. (2017). Proximal Policy Optimization Algorithms. arXiv:1707.06347.

Lepikhin, D., et al. (2021). GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding. ICLR 2021.