{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b64ed95-f2d5-440e-b90b-bfd083d63716",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "9b488857",
   "metadata": {},
   "source": [
    "# Guardar multilingual-e5-small en local y benchmark de carga\n",
    "\n",
    "Guardamos el modelo en `.local/models/` (gitignored) y comparamos:\n",
    "- Carga desde HuggingFace cache vs carga desde path local\n",
    "- Tiempo de primer encoding tras carga\n",
    "- Verificación de que los embeddings son idénticos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "22b75c11",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "FN_REGISTRY_ROOT: /home/lucas/fn_registry\n",
      "Destino local:    /home/lucas/fn_registry/.local/models/multilingual-e5-small\n",
      "RAM actual:       833 MB\n"
     ]
    }
   ],
   "source": [
    "import os, time, gc\n",
    "import numpy as np\n",
    "from sentence_transformers import SentenceTransformer\n",
    "import psutil\n",
    "\n",
    "FN_ROOT = os.environ.get('FN_REGISTRY_ROOT', '/home/lucas/fn_registry')\n",
    "MODEL_ID = 'intfloat/multilingual-e5-small'\n",
    "LOCAL_PATH = os.path.join(FN_ROOT, '.local', 'models', 'multilingual-e5-small')\n",
    "\n",
    "def ram_mb():\n",
    "    return psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024)\n",
    "\n",
    "print(f'FN_REGISTRY_ROOT: {FN_ROOT}')\n",
    "print(f'Destino local:    {LOCAL_PATH}')\n",
    "print(f'RAM actual:       {ram_mb():.0f} MB')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00e8cb16",
   "metadata": {},
   "source": [
    "## 1. Guardar modelo en .local/models/"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "90df0aac",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cargando modelo desde HuggingFace cache...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "c21758811d8c41f28cec7ecf11df90ed",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mBertModel LOAD REPORT\u001b[0m from: intfloat/multilingual-e5-small\n",
      "Key                     | Status     |  | \n",
      "------------------------+------------+--+-\n",
      "embeddings.position_ids | UNEXPECTED |  | \n",
      "\n",
      "Notes:\n",
      "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  Cargado en 6.43s\n",
      "Guardando en /home/lucas/fn_registry/.local/models/multilingual-e5-small...\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "cb775051d2b64cfcad0f9f0a4302a2c4",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Writing model shards:   0%|          | 0/1 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  Guardado en 2.33s\n",
      "  Tamaño en disco: 465.6 MB\n",
      "  multilingual-e5-small/\n",
      "    README.md (0.5 MB)\n",
      "    config.json (0.0 MB)\n",
      "    config_sentence_transformers.json (0.0 MB)\n",
      "    model.safetensors (448.8 MB)\n",
      "    modules.json (0.0 MB)\n",
      "    sentence_bert_config.json (0.0 MB)\n",
      "    tokenizer.json (16.3 MB)\n",
      "    tokenizer_config.json (0.0 MB)\n",
      "    1_Pooling/\n",
      "      config.json (0.0 MB)\n",
      "    2_Normalize/\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "4063"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Cargar desde HF cache y guardar localmente\n",
    "print('Cargando modelo desde HuggingFace cache...')\n",
    "t0 = time.perf_counter()\n",
    "model = SentenceTransformer(MODEL_ID)\n",
    "hf_load_time = time.perf_counter() - t0\n",
    "print(f'  Cargado en {hf_load_time:.2f}s')\n",
    "\n",
    "# Guardar en .local/models/\n",
    "print(f'Guardando en {LOCAL_PATH}...')\n",
    "t0 = time.perf_counter()\n",
    "model.save(LOCAL_PATH)\n",
    "save_time = time.perf_counter() - t0\n",
    "print(f'  Guardado en {save_time:.2f}s')\n",
    "\n",
    "# Verificar tamaño en disco\n",
    "total_size = 0\n",
    "for dirpath, dirnames, filenames in os.walk(LOCAL_PATH):\n",
    "    for f in filenames:\n",
    "        fp = os.path.join(dirpath, f)\n",
    "        total_size += os.path.getsize(fp)\n",
    "print(f'  Tamaño en disco: {total_size / (1024*1024):.1f} MB')\n",
    "\n",
    "# Listar archivos guardados\n",
    "for dirpath, dirnames, filenames in os.walk(LOCAL_PATH):\n",
    "    level = dirpath.replace(LOCAL_PATH, '').count(os.sep)\n",
    "    indent = '  ' * level\n",
    "    subdir = os.path.basename(dirpath)\n",
    "    print(f'  {indent}{subdir}/')\n",
    "    for f in sorted(filenames):\n",
    "        size = os.path.getsize(os.path.join(dirpath, f))\n",
    "        print(f'  {indent}  {f} ({size/(1024*1024):.1f} MB)')\n",
    "\n",
    "del model\n",
    "gc.collect()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "876b8c22",
   "metadata": {},
   "source": [
    "## 2. Benchmark: carga local vs HF cache\n",
    "\n",
    "Cargamos 5 veces de cada fuente para tener estadísticas estables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "42346646",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Benchmark de carga (5 iteraciones cada uno)...\n",
      "\n",
      "→ Cargando desde HuggingFace cache...\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "0859bcd797a94dca8afdc0341ed73044",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mBertModel LOAD REPORT\u001b[0m from: intfloat/multilingual-e5-small\n",
      "Key                     | Status     |  | \n",
      "------------------------+------------+--+-\n",
      "embeddings.position_ids | UNEXPECTED |  | \n",
      "\n",
      "Notes:\n",
      "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "39c9cc105c904dd39fcf08889b33ba98",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mBertModel LOAD REPORT\u001b[0m from: intfloat/multilingual-e5-small\n",
      "Key                     | Status     |  | \n",
      "------------------------+------------+--+-\n",
      "embeddings.position_ids | UNEXPECTED |  | \n",
      "\n",
      "Notes:\n",
      "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "6c777e538f764bc99cca408f6242eeda",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mBertModel LOAD REPORT\u001b[0m from: intfloat/multilingual-e5-small\n",
      "Key                     | Status     |  | \n",
      "------------------------+------------+--+-\n",
      "embeddings.position_ids | UNEXPECTED |  | \n",
      "\n",
      "Notes:\n",
      "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "16d63aeb9fcb4d5f8e8d330931ff7d4b",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mBertModel LOAD REPORT\u001b[0m from: intfloat/multilingual-e5-small\n",
      "Key                     | Status     |  | \n",
      "------------------------+------------+--+-\n",
      "embeddings.position_ids | UNEXPECTED |  | \n",
      "\n",
      "Notes:\n",
      "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "6b7b24c69e87464ea2efd25156b77170",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mBertModel LOAD REPORT\u001b[0m from: intfloat/multilingual-e5-small\n",
      "Key                     | Status     |  | \n",
      "------------------------+------------+--+-\n",
      "embeddings.position_ids | UNEXPECTED |  | \n",
      "\n",
      "Notes:\n",
      "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  Media: 4.134s ± 0.211s (min: 3.797s)\n",
      "→ Cargando desde .local/models/...\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "c12421a330124488ad628e6175f3229a",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "a36175de726e4c13ba1cadb89186af2c",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e0507871bb0648a7910f9521856ece72",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "9f5aa78b20d34492a2e924445d4ed769",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "cb904177545448d5b83aea15f8f15b40",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  Media: 1.821s ± 0.138s (min: 1.563s)\n",
      "\n",
      "Speedup local vs HF cache: 2.27x\n"
     ]
    }
   ],
   "source": [
    "N_RUNS = 5\n",
    "\n",
    "def bench_load(source, path, runs=N_RUNS):\n",
    "    \"\"\"Carga modelo N veces, retorna tiempos y RAM.\"\"\"\n",
    "    times = []\n",
    "    rams = []\n",
    "    for i in range(runs):\n",
    "        gc.collect()\n",
    "        ram_before = ram_mb()\n",
    "        t0 = time.perf_counter()\n",
    "        m = SentenceTransformer(path)\n",
    "        elapsed = time.perf_counter() - t0\n",
    "        ram_after = ram_mb()\n",
    "        times.append(elapsed)\n",
    "        rams.append(ram_after - ram_before)\n",
    "        del m\n",
    "        gc.collect()\n",
    "    return {\n",
    "        'source': source,\n",
    "        'times': times,\n",
    "        'mean_time': np.mean(times),\n",
    "        'std_time': np.std(times),\n",
    "        'min_time': np.min(times),\n",
    "        'mean_ram_delta': np.mean(rams),\n",
    "    }\n",
    "\n",
    "print(f'Benchmark de carga ({N_RUNS} iteraciones cada uno)...')\n",
    "print()\n",
    "\n",
    "# Carga desde HF cache\n",
    "print('→ Cargando desde HuggingFace cache...')\n",
    "hf_bench = bench_load('HF cache', MODEL_ID)\n",
    "print(f'  Media: {hf_bench[\"mean_time\"]:.3f}s ± {hf_bench[\"std_time\"]:.3f}s (min: {hf_bench[\"min_time\"]:.3f}s)')\n",
    "\n",
    "# Carga desde path local\n",
    "print('→ Cargando desde .local/models/...')\n",
    "local_bench = bench_load('Local path', LOCAL_PATH)\n",
    "print(f'  Media: {local_bench[\"mean_time\"]:.3f}s ± {local_bench[\"std_time\"]:.3f}s (min: {local_bench[\"min_time\"]:.3f}s)')\n",
    "\n",
    "speedup = hf_bench['mean_time'] / local_bench['mean_time']\n",
    "print(f'\\nSpeedup local vs HF cache: {speedup:.2f}x')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06291006",
   "metadata": {},
   "source": [
    "## 3. Verificación: embeddings idénticos\n",
    "\n",
    "Confirmamos que el modelo local produce exactamente los mismos embeddings que el de HF cache."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "8e6eaa4a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "a469b638479a45deb836f54d1edaa056",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mBertModel LOAD REPORT\u001b[0m from: intfloat/multilingual-e5-small\n",
      "Key                     | Status     |  | \n",
      "------------------------+------------+--+-\n",
      "embeddings.position_ids | UNEXPECTED |  | \n",
      "\n",
      "Notes:\n",
      "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "2da4a0a304ba40a0a4748941dbe4ea9a",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading weights:   0%|          | 0/199 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Max diferencia absoluta:  0.00e+00\n",
      "Mean diferencia absoluta: 0.00e+00\n",
      "¿Son idénticos (atol=1e-6)? ✓ SÍ\n",
      "  [0] cosine=1.0000000000 | passage: La inteligencia artificial transforma la industria \n",
      "  [1] cosine=1.0000000000 | passage: Los mercados financieros reaccionan a las decisione\n",
      "  [2] cosine=1.0000000000 | passage: El aprendizaje profundo requiere grandes cantidades\n",
      "  [3] cosine=1.0000000000 | query: ¿Cómo funciona el machine learning?\n",
      "  [4] cosine=1.0000000000 | query: ¿Qué afecta los precios de las acciones?\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "6961"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "test_texts = [\n",
    "    'passage: La inteligencia artificial transforma la industria tecnológica',\n",
    "    'passage: Los mercados financieros reaccionan a las decisiones de política monetaria',\n",
    "    'passage: El aprendizaje profundo requiere grandes cantidades de datos etiquetados',\n",
    "    'query: ¿Cómo funciona el machine learning?',\n",
    "    'query: ¿Qué afecta los precios de las acciones?',\n",
    "]\n",
    "\n",
    "# Cargar ambos modelos\n",
    "model_hf = SentenceTransformer(MODEL_ID)\n",
    "model_local = SentenceTransformer(LOCAL_PATH)\n",
    "\n",
    "# Generar embeddings\n",
    "emb_hf = model_hf.encode(test_texts, normalize_embeddings=True)\n",
    "emb_local = model_local.encode(test_texts, normalize_embeddings=True)\n",
    "\n",
    "# Comparar\n",
    "max_diff = np.max(np.abs(emb_hf - emb_local))\n",
    "mean_diff = np.mean(np.abs(emb_hf - emb_local))\n",
    "are_equal = np.allclose(emb_hf, emb_local, atol=1e-6)\n",
    "\n",
    "print(f'Max diferencia absoluta:  {max_diff:.2e}')\n",
    "print(f'Mean diferencia absoluta: {mean_diff:.2e}')\n",
    "print(f'¿Son idénticos (atol=1e-6)? {\"✓ SÍ\" if are_equal else \"✗ NO\"}')\n",
    "\n",
    "# Cosine similarity entre pares correspondientes\n",
    "for i, text in enumerate(test_texts):\n",
    "    sim = np.dot(emb_hf[i], emb_local[i])\n",
    "    print(f'  [{i}] cosine={sim:.10f} | {text[:60]}')\n",
    "\n",
    "del model_hf, model_local\n",
    "gc.collect()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c139a193",
   "metadata": {},
   "source": [
    "## 4. Visualización de tiempos de carga"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "53664d2a",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/tmp/ipykernel_18429/1763775165.py:10: MatplotlibDeprecationWarning: The 'labels' parameter of boxplot() has been renamed 'tick_labels' since Matplotlib 3.9; support for the old name will be dropped in 3.11.\n",
      "  bp = axes[0].boxplot(data, labels=labels, patch_artist=True)\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 43308 characters]",
      "text/plain": [
       "<Figure size 1400x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data",
     "transient": {}
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
    "\n",
    "# Box plot de tiempos\n",
    "data = [hf_bench['times'], local_bench['times']]\n",
    "labels = ['HF cache', 'Local (.local/)']\n",
    "colors = ['#e74c3c', '#2ecc71']\n",
    "\n",
    "bp = axes[0].boxplot(data, labels=labels, patch_artist=True)\n",
    "for patch, color in zip(bp['boxes'], colors):\n",
    "    patch.set_facecolor(color)\n",
    "    patch.set_alpha(0.7)\n",
    "axes[0].set_ylabel('Tiempo de carga (s)')\n",
    "axes[0].set_title('Distribución de tiempos de carga')\n",
    "\n",
    "# Bar chart comparativo\n",
    "means = [hf_bench['mean_time'], local_bench['mean_time']]\n",
    "stds = [hf_bench['std_time'], local_bench['std_time']]\n",
    "bars = axes[1].bar(labels, means, yerr=stds, color=colors, alpha=0.8, capsize=10)\n",
    "for bar, mean in zip(bars, means):\n",
    "    axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05,\n",
    "                 f'{mean:.3f}s', ha='center', fontsize=12, fontweight='bold')\n",
    "axes[1].set_ylabel('Tiempo medio (s)')\n",
    "axes[1].set_title(f'Carga media ({N_RUNS} runs) — speedup: {speedup:.2f}x')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
