Metadata-Version: 2.4
Name: micronnx
Version: 0.2.1.0
Summary: micronnx — runtime de inferencia puro NumPy para extracción de pesos, capas y activaciones de LLMs y CNNs. Parte del ecosistema UFM (Unified Fusion Model). Soporta GGUF (Q2_K-Q6_K), SafeTensors, HDF5/Keras y NPZ sin PyTorch ni TensorFlow.
License-Expression: MIT
Project-URL: Repository, https://github.com/tuusuario/micronnx
Keywords: llm,inference,numpy,gguf,safetensors,hdf5,mobilenet,activation-extraction,model-fusion,quantization
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: pyfive
Dynamic: license-file

<p align="center">
  <img src="https://raw.githubusercontent.com/lmontanohernandez8-png/Micronnx/main/logo.png" width="600"/>
</p>

# micronnx

> Capa de extracción de pesos, capas y activaciones para sistemas de fusión de modelos.
> Parte del ecosistema **UFM** (Unified Fusion Model).

micronnx no es un framework de entrenamiento ni un motor de inferencia general.
Es una pieza de recolección: carga cualquier modelo desde cualquier formato, extrae sus pesos y activaciones capa a capa, y los expone en una estructura unificada lista para que UFM tome decisiones de fusión.

**Sin PyTorch. Sin TensorFlow. Solo NumPy.**

---

## Instalación

    pip install micronnx

---

## ¿Qué hace micronnx?

- Carga de modelos: GGUF (Q2_K a Q6_K, Q8_K, Q8_0, Q8_1, IQ4_NL, BF16, F16, F32), SafeTensors, HDF5/Keras, NPY/NPZ — todos lazy, sin cargar nada hasta pedirlo
- Extracción de pesos: todos los tensores normalizados a float32, etiquetados con rol y capa
- Extracción de activaciones: capa a capa — embed, attn, ffn, residual, norm, pool
- Exportación unificada: uno o varios modelos a un solo .npz con índice completo (schema y hp incluidos)
- Carga desde .npz: NpzModelLoader detecta arquitectura automáticamente (31 familias) incluso en .npz de versiones antiguas sin schema_name
- Ops vectorizadas: RMSNorm, Attention GQA, RoPE, SwiGLU, GeGLU, Conv2D, GroupNorm y más — sin loops Python
- LLM decoder-only (LLaMA, Mistral, Gemma, Phi, ...) vía ModelRunner, y encoder-decoder (T5, Flan-T5, mT5, UL2) vía T5Runner
- Sliding window real (Gemma 2/3, Mistral) — antes se detectaba en metadata y se ignoraba en el forward
- Visión: además de MobileNet (v1), ahora ResNet (18/34/50/101/152), EfficientNet (B0-B7) y Vision Transformer (B/16 a H/14)

---

## Formatos soportados

GGUF (.gguf)
  Q2_K, Q3_K, Q4_0, Q4_1, Q4_K, Q5_0, Q5_1, Q5_K, Q6_K, Q8_K
  Q8_0, Q8_1, IQ4_NL, BF16, F16, F32
  No incluidos todavía: IQ4_XS, i-quants de codebook (IQ2_XXS/XS/S,
  IQ3_XXS/S, IQ1_S/M), TQ1_0/TQ2_0 — ver "Limitaciones conocidas"

SafeTensors (.safetensors)
  F32, F16, BF16 (corregido en v2), F64 convertido a F32 automaticamente
  I8, I16, I32, I64 sin conversion

HDF5 / Keras (.h5, .keras)
  float32, float64 convertido a float32, lazy loading

NumPy (.npy, .npz)
  float32, float16 convertido a float32, mmap lazy para .npz

---

## Arquitecturas detectadas automaticamente

GGUF:
  gguf_llama, gguf_gemma, gguf_gemma2, gguf_phi3
  gguf_falcon, gguf_falcon40b, gguf_gpt2, gguf_gpt_neox
  gguf_bloom, gguf_mpt, gguf_bert, gguf_mixtral
  gguf_qwen_moe, gguf_olmo2

HuggingFace SafeTensors:
  hf_llama, hf_gemma, hf_gemma2, hf_gemma3, hf_phi2, hf_phi3
  hf_falcon, hf_falcon40b, hf_gpt2, hf_gpt_neox
  hf_bloom, hf_mpt, hf_bert, hf_mixtral, hf_qwen_moe
  hf_chatglm, hf_cohere, hf_olmo2, hf_internlm2
  hf_baichuan, hf_stablelm, hf_minicpm, hf_xverse, hf_t5

  NOTA: hf_t5 corre de verdad con nx.T5Runner (encoder-decoder, ver
  seccion dedicada mas abajo) — en versiones anteriores el schema
  estaba declarado pero ModelRunner lo rechazaba con NotImplementedError.
  ModelRunner sigue sin aceptar hf_t5 (correcto: es decoder-only, T5 no
  encaja ahi); usa T5Runner para esta familia.

  gemma3 se detecta de forma diferenciada de gemma2 (patron de sliding
  window 5:1 vs 1:1) tanto desde config.json (detect_schema_hf) como
  desde los nombres de tensor sin config (detect_schema_safetensors,
  via la presencia de self_attn.q_norm/k_norm, exclusivos de Gemma3).

Cubre: LLaMA 1/2/3/3.1/3.2, Mistral, Qwen 1/2/2.5/3, SmolLM, Gemma 1/2/3,
Phi-2/3/3.5/4, Falcon 7B/40B, GPT-2, Starcoder2, GPT-NeoX, Pythia, BLOOM,
MPT, BERT, RoBERTa, DeBERTa, Mixtral, Qwen2-MoE, DeepSeek V2/V3,
ChatGLM4, Cohere Command-R, Aya, OLMo2, InternLM2, Baichuan2,
StableLM, MiniCPM, XVERSE, T5, mT5, umT5, T5v1.1, Flan-T5, UL2,
MobileNetV1, MobileNetV2, MobileNetV3, ResNet18/34/50/101/152,
EfficientNet B0-B7 (V1), ViT-B/16, ViT-B/32, ViT-L/16, ViT-L/32, ViT-H/14

  NOTA: BART NO esta soportado, pese a ser tambien encoder-decoder.
  En versiones anteriores "bart" apuntaba por error al mismo schema que
  T5 (hf_t5), pese a que BART usa posiciones absolutas aprendidas + GELU,
  no bias posicional relativo por bucket logaritmico como T5 -- son
  matematicas de forward distintas. Ese mapeo se retiro; un checkpoint
  BART hoy cae en el fallback hf_llama y falla con KeyError (ruidoso),
  en vez de correr en silencio con resultados incorrectos como antes.
  Soporte real de BART queda pendiente.

---

## Uso rapido

    import micronnx as nx

    loader     = nx.GGUFLoader("model.gguf")
    schema, hp = nx.detect_schema_gguf("model.gguf")
    runner     = nx.ModelRunner(loader, schema, hp)
    logits     = runner.forward(input_ids)

---

## T5 / Flan-T5 / mT5 / UL2 — encoder-decoder

T5 es una arquitectura distinta a todo lo demas en este README: dos
torres (encoder + decoder) con cross-attention entre ambas, y bias
posicional relativo por bucket logaritmico en vez de RoPE. Por eso usa
un runner propio, nx.T5Runner, en vez de nx.ModelRunner.

    import numpy as np
    import micronnx as nx

    loader     = nx.SafeTensorsLoader("flan-t5-small/model.safetensors")
    schema, hp = nx.detect_schema_hf("flan-t5-small/config.json")
    t5         = nx.T5Runner(loader, schema, hp, max_seq=512)

    # El encoder se corre UNA VEZ por secuencia de entrada
    encoder_ids = np.array([[...]], dtype=np.int64)
    t5.encode(encoder_ids)

    # El decoder corre paso a paso, con KV cache incremental
    # (igual de valido pasar el prefijo completo de una vez que ir
    # token a token -- ambos caminos dan el mismo resultado numerico)
    decoder_ids = np.array([[0]], dtype=np.int64)   # decoder_start_token_id
    logits      = t5.forward(decoder_ids)
    next_token  = int(logits[0, -1].argmax())

    # Para una nueva secuencia de entrada, limpiar el estado primero
    t5.reset()

Cubre T5 clasico (ReLU, sin puerta) y T5v1.1/Flan-T5/UL2 (GeGLU,
wi_0/wi_1) automaticamente, detectado desde config.json
(feed_forward_proj: "relu" o "gated-gelu") o inferido con el default
"relu" si el config no trae ese campo.

---

## Visión — ResNet, EfficientNet, ViT

    import numpy as np
    import micronnx as nx

    image = np.random.uniform(0, 1, (224, 224, 3)).astype(np.float32)

    # ResNet — 18, 34, 50, 101 o 152 capas
    loader = nx.SafeTensorsLoader("resnet50/model.safetensors")
    resnet = nx.ResNetRunner(loader, depth=50, num_classes=1000)
    logits = resnet.forward(image)   # SIN softmax, igual que torchvision

    # EfficientNet — b0 a b7 (V1; V2 no soportado, ver limitaciones)
    loader = nx.SafeTensorsLoader("efficientnet_b0/model.safetensors")
    effnet = nx.EfficientNetRunner(loader, variant="b0", num_classes=1000)
    logits = effnet.forward(image)

    # Vision Transformer — b_16, b_32, l_16, l_32, h_14
    loader = nx.SafeTensorsLoader("vit_b_16/model.safetensors")
    vit    = nx.ViTRunner(loader, variant="b_16", num_classes=1000, image_size=224)
    logits = vit.forward(image)

    # Con extraccion de activaciones (misma convencion on_activation
    # que ModelRunner/CNNRunner)
    acts = {}
    logits = resnet.forward(image, on_activation=lambda k, t: acts.__setitem__(k, t))
    print(acts.keys())
    # stem_conv, stem_pool, layer1.0_out, layer1.1_out, ..., pooled, logits

Los tres runners usan nombres de tensor y estructura de torchvision
(no el puerto de HuggingFace, que usa nomenclatura distinta). ViTRunner
requiere que la imagen coincida exactamente con image_size (no
interpola pos_embedding para tamaños arbitrarios, a diferencia de
torchvision).

---

## Exportar modelos a .npz

    import micronnx as nx

    # Un solo modelo
    nx.export_to_npz("model.gguf", "model.npz")

    # Varios modelos, un .npz por cada uno
    nx.export_to_npz(
        ["model.gguf", "model.safetensors", "mobilenet.h5"],
        "outputs/"
    )

    # Varios modelos en un solo .npz fusionado
    nx.export_to_npz(
        ["model.gguf", "model.safetensors", "mobilenet.h5"],
        "outputs/merged.npz",
        merge=True
    )

    # Con string separado por comas
    nx.export_to_npz("model.gguf, model.safetensors", "outputs/", merge=False)

---

## Inspeccionar un .npz

    import micronnx as nx

    nx.inspect_npz("outputs/merged.npz")
    # Merged  : 2 modelos
    # Total   : 269,030,016 params | 513.14 MB float16
    # [SmolLM2-135M]  272 tensores | 30 capas | 256.57 MB | schema: gguf_llama
    # [model]         272 tensores | 30 capas | 256.57 MB | schema: hf_llama

    # Leer el indice sin cargar ningun tensor
    idx = nx.load_index("outputs/merged.npz")
    print(idx["n_models"])
    print(idx["total_params"])
    print(idx["models"].keys())

    # Schema e hiperparametros de cada modelo
    for name, info in idx["models"].items():
        print(name, info["schema_name"], info["hp"])

---

## Cargar desde .npz sin archivo original

    import micronnx as nx

    # Ver que modelos hay
    models = nx.list_models("outputs/merged.npz")
    for name, info in models.items():
        print(name, info["schema_name"], info["has_hp"], info["has_activations"])

    # Cargar directamente — detecta arquitectura automaticamente
    loader     = nx.NpzModelLoader("outputs/merged.npz", "SmolLM2-135M-Instruct-Q4_K_M")
    schema, hp = nx.detect_schema_npz("outputs/merged.npz", "SmolLM2-135M-Instruct-Q4_K_M")
    runner     = nx.ModelRunner(loader, schema, hp, max_seq=512)

    # Si el .npz no tiene schema_name (version antigua),
    # se detecta automaticamente desde los nombres de tensores sin el archivo original

---

## Extraer activaciones — LLM

    import numpy as np
    import micronnx as nx

    # Desde archivo original
    loader     = nx.GGUFLoader("SmolLM2-135M-Instruct-Q4_K_M.gguf")
    schema, hp = nx.detect_schema_gguf("SmolLM2-135M-Instruct-Q4_K_M.gguf")
    runner     = nx.ModelRunner(loader, schema, hp, max_seq=512)

    # O desde .npz sin archivo original
    loader     = nx.NpzModelLoader("outputs/merged.npz", "SmolLM2-135M-Instruct-Q4_K_M")
    schema, hp = nx.detect_schema_npz("outputs/merged.npz", "SmolLM2-135M-Instruct-Q4_K_M")
    runner     = nx.ModelRunner(loader, schema, hp, max_seq=512)

    # Todas las activaciones
    ext = nx.ActivationExtractor(runner)
    ext.run(np.array([[1, 2, 3, 4, 5]], dtype=np.int64))
    print(ext.keys())
    # embed, attn_norm_0, post_attn_0, residual_attn_0,
    # ffn_norm_0, post_ffn_0, residual_ffn_0, ..., final_norm

    # One-shot sin instanciar
    logits, acts = nx.ActivationExtractor.extract(
        runner,
        np.array([[1, 2, 3]], dtype=np.int64)
    )

    # Solo algunos hooks
    ext = nx.ActivationExtractor(runner, hooks=["post_attn", "residual_ffn"])
    ext.run(np.array([[1, 2, 3, 4, 5]], dtype=np.int64))

    # Reducir la dimension de secuencia
    ext = nx.ActivationExtractor(runner, reduce="last")   # ultimo token
    ext = nx.ActivationExtractor(runner, reduce="mean")   # media de tokens

    # Solo capas pares
    ext = nx.ActivationExtractor(runner, layer_fn=lambda i: i % 2 == 0)

    # Solo capas especificas por indice
    ext = nx.ActivationExtractor(runner, layer_fn=[0, 5, 11, 23])

    # Acceso a activaciones
    hidden = ext.get("final_norm")       # KeyError claro si no fue capturado
    attn5  = ext.get("post_attn_5")
    for key, tensor in ext.items():
        print(key, tensor.shape)

---

## Extraer activaciones — BERT

    import numpy as np
    import micronnx as nx

    loader     = nx.SafeTensorsLoader("bert-base-uncased/model.safetensors")
    schema, hp = nx.detect_schema_safetensors("bert-base-uncased/model.safetensors")
    runner     = nx.ModelRunner(loader, schema, hp)

    ext    = nx.ActivationExtractor(runner, hooks=["post_attn", "final_norm"])
    hidden = ext.run_bert(
        np.array([[101, 2054, 2003, 102]], dtype=np.int64),
        attention_mask=np.ones((1, 4), dtype=np.int64)
    )

    # One-shot para BERT
    hidden, acts = nx.ActivationExtractor.extract_bert(runner, input_ids)
    cls_vector   = acts["final_norm"][0, 0]   # token [CLS]

---

## Extraer activaciones — CNN (MobileNet)

    import numpy as np
    import micronnx as nx

    raw    = nx.H5Loader("mobilenet_1_0_224_tf.h5")
    mapped = nx.map_tensors(dict.fromkeys(raw.tensor_names), fmt="h5")
    loader = nx.CanonicalLoader(raw, mapped)
    runner = nx.CNNRunner(loader, n_blocks=13)

    # Imagen 224x224x3 normalizada en [-1, 1]
    image = np.random.uniform(-1, 1, (224, 224, 3)).astype(np.float32)

    # Forward directo
    probs = runner.forward(image)
    print(f"clase: {probs.argmax()}, confianza: {probs.max():.3f}")

    # Con extraccion de activaciones
    ext   = nx.CNNActivationExtractor(runner, reduce="spatial")
    probs = ext.run(image)
    print(ext.keys())
    # stem, block_0_dw, block_0_pw, ..., block_12_pw, pooled

    # One-shot
    probs, acts = nx.CNNActivationExtractor.extract(runner, image)
    feat = acts["block_5_pw"]   # (C,) con reduce="spatial"

    # MobileNetV2 / MobileNetV3
    runner = nx.InvertedResidualRunner(loader, n_blocks=17)
    probs  = runner.forward(image)

---

## Guardar y leer activaciones en el .npz

    import numpy as np
    import micronnx as nx

    merged = nx.export_to_npz(
        ["SmolLM2-135M-Instruct-Q4_K_M.gguf", "model.safetensors"],
        "outputs/merged.npz",
        merge=True
    )

    loader     = nx.NpzModelLoader(merged, "SmolLM2-135M-Instruct-Q4_K_M")
    schema, hp = nx.detect_schema_npz(merged, "SmolLM2-135M-Instruct-Q4_K_M")
    runner     = nx.ModelRunner(loader, schema, hp)

    ext = nx.ActivationExtractor(runner, reduce="last")
    ext.run(np.array([[1, 2, 3]], dtype=np.int64))

    nx.save_activations(merged, ext.activations, model_key="SmolLM2-135M-Instruct-Q4_K_M")

    # Leer despues sin recargar el modelo
    acts = nx.load_activations(merged, model_key="SmolLM2-135M-Instruct-Q4_K_M")
    print(acts["final_norm"].shape)

---

## TensorRegistry — hasta 3 modelos lazy simultaneos

    import micronnx as nx

    # Registrar modelos — solo indexa nombres, no carga datos
    nx.registry.register("llama3",   nx.GGUFLoader("llama3.gguf"),         fmt="gguf")
    nx.registry.register("mistral",  nx.SafeTensorsLoader("mistral.st"),   fmt="safetensors")
    nx.registry.register("mobilenet",nx.H5Loader("mobilenet.h5"),          fmt="h5")

    # Consultar sin cargar nada — O(1)
    nx.registry.has("llama3", "layers.5.attn.q.weight")   # True / False
    nx.registry.list("llama3")                             # lista de canonicos

    # Cargar un tensor — solo aqui se lee del disco
    t = nx.registry.get("llama3", "layers.5.attn.q.weight")

    # Al registrar un 4to modelo, el mas antiguo se expulsa automaticamente (LRU)
    nx.registry.release("mistral")   # liberar manualmente
    nx.registry.stats()              # {"models": [...], "slots_used": 2, ...}

    # O instanciar un registry propio
    reg = nx.TensorRegistry()
    reg.register("modelo_a", loader_a)
    reg.register("modelo_b", loader_b)

---

## Loaders directos

    import micronnx as nx

    # GGUF — lazy, mmap, dequantiza bajo demanda
    loader = nx.GGUFLoader("model.gguf")
    print(loader.tensor_names[:5])
    w = loader.load("token_embd.weight")
    loader.close()

    # SafeTensors — lazy, mmap, BF16/F64 correctos
    loader = nx.SafeTensorsLoader("model.safetensors")
    print(loader.shape("model.embed_tokens.weight"))    # sin cargar
    print(loader.dtype("model.embed_tokens.weight"))    # sin cargar
    w = loader.load("model.embed_tokens.weight")

    # Cargar solo tensores de atencion (ahorra 30-70% de RAM)
    weights = loader.load_all(filter_fn=lambda n: "attn" in n)

    # Exportar sin pico de RAM — un tensor a la vez
    for name, arr in loader.stream_load(keep_float16=True):
        pass   # procesar arr y liberar

    loader.close()

    # HDF5 / Keras — lazy, sin cargar hasta .load()
    loader = nx.H5Loader("mobilenet.h5")
    w = loader.load("conv1/kernel:0")
    attrs = loader.attributes("conv1")   # metadata de la capa Keras
    loader.close()

    # NPY / NPZ — mmap lazy para .npz
    loader = nx.NpyLoader("weights.npz")
    w = loader.load("layer_0")

    # Directorio de .npy (estilo JAX/Flax)
    loader = nx.NpyLoader("checkpoints/")
    w = loader.load("encoder/layers/0/attn/q")

    # Desde .npz merged sin archivo original
    loader = nx.NpzModelLoader("outputs/merged.npz", "model")
    w = loader.load("model.embed_tokens.weight")
    loader.close()

---

## Detectar schema y arquitectura

    import micronnx as nx

    # Desde archivo original
    schema, hp = nx.detect_schema_gguf("model.gguf")
    print(schema)
    # gguf_llama / gguf_gemma2 / gguf_phi3 / gguf_mixtral / gguf_qwen_moe ...
    print(hp)
    # {"n_layers": 30, "n_heads": 9, "n_kv_heads": 3, "n_embd": 576, "vocab_size": 49152}

    schema, hp = nx.detect_schema_safetensors("model.safetensors")
    schema, hp = nx.detect_schema_hf("model.safetensors")   # alias

    # Desde .npz sin archivo original
    schema, hp = nx.detect_schema_npz("outputs/merged.npz", "SmolLM2-135M-Instruct-Q4_K_M")

    # Ver todos los schemas disponibles
    print(list(nx.SCHEMAS.keys()))
    # gguf_llama, gguf_gemma, gguf_gemma2, gguf_phi3, gguf_falcon,
    # gguf_mixtral, gguf_qwen_moe, hf_llama, hf_gemma2, hf_bert ...

---

## Mapeo canonico de tensores

    import micronnx as nx

    # Convertir nombres originales a nombres canonicos
    loader = nx.GGUFLoader("model.gguf")
    mapped = nx.map_tensors(dict.fromkeys(loader.tensor_names), fmt="gguf")

    # Ver tensores sin mapear
    unmapped = nx.find_unmapped(dict.fromkeys(loader.tensor_names), fmt="gguf", mapped=mapped)

    # Resolver embeddings atados (embed <-> head)
    mapped = nx.resolve_tied_embeddings(mapped)

    # Detectar formato automaticamente
    fmt = nx.detect_format(dict.fromkeys(loader.tensor_names))

    # CanonicalLoader: traduce nombres canonicos a originales automaticamente
    canonical = nx.CanonicalLoader(loader, mapped)
    w = canonical.load("layers.0.attn.q.weight")   # nombre canonico
    print("layers.0.attn.q.weight" in canonical)   # True

---

## Ops NumPy directas

    import numpy as np
    import micronnx as nx

    x = np.random.randn(1, 16, 576).astype(np.float32)
    w = np.ones(576, dtype=np.float32)

    # Normalizacion
    x = nx.rmsnorm(x, w)
    x = nx.layernorm(x, w, w)
    x = nx.batchnorm(x, gamma, beta, mean, var)
    x = nx.group_norm(x, num_groups=8, weight=w)
    x = nx.instance_norm(x)

    # Activaciones
    x = nx.gelu(x)
    x = nx.quick_gelu(x)       # CLIP, ViT
    x = nx.silu(x)             # LLaMA, Mistral
    x = nx.mish(x)             # YOLOv4
    x = nx.hardswish(x)        # MobileNetV3
    x = nx.leaky_relu(x, 0.01) # GANs, YOLO
    x = nx.prelu(x, weight)    # ResNet
    x = nx.elu(x)

    # FFN
    out = nx.swiglu(x, gate_w, up_w, down_w)          # LLaMA, Mistral, Qwen
    out = nx.swiglu_fused(x, gate_up_w, down_w)       # Phi-3, InternLM2
    out = nx.geglu(x, gate_w, up_w, down_w)           # Gemma
    out = nx.geglu_fused(x, gate_up_w, down_w)        # T5v1.1, Flan-T5
    out = nx.ffn_gelu(x, up_w, down_w)                # GPT-2, BLOOM
    out = nx.ffn_relu(x, up_w, down_w)                # T5 original

    # Atencion GQA (cubre MHA y MQA como casos especiales)
    q   = np.random.randn(1, 4, 9, 64).astype(np.float32)
    k   = np.random.randn(1, 4, 3, 64).astype(np.float32)
    v   = np.random.randn(1, 4, 3, 64).astype(np.float32)
    out = nx.attention(q, k, v, n_heads=9, n_kv_heads=3)

    # SDPA directa sin reshape de heads (ViT, CLIP)
    out = nx.scaled_dot_product(q, k, v, mask=None)

    # RoPE — convencion LLaMA correcta
    x   = np.random.randn(1, 4, 8, 64).astype(np.float32)
    out = nx.rope(x, pos=0)
    out = nx.rope(x, positions=np.array([0, 1, 2, 3]))

    # CNN
    img  = np.random.randn(112, 112, 32).astype(np.float32)
    filt = np.random.randn(64, 32, 3, 3).astype(np.float32)   # formato PyTorch (C_out,C_in,kH,kW)
    out  = nx.conv2d(img, filt, stride=1, padding=1)
    out  = nx.depthwise_conv2d(img, dw_weight, stride=2)
    out  = nx.global_avg_pool(out)
    out  = nx.avg_pool2d(out, kernel_size=2)
    out  = nx.max_pool2d(out, size=2, stride=2)
    out  = nx.adaptive_avg_pool(out, output_size=(7, 7))
    out  = nx.upsample_nearest(out, scale_factor=2)

---

## API completa

Loaders
  nx.GGUFLoader(path)                       lazy, mmap, dequant bajo demanda
  nx.SafeTensorsLoader(path)                lazy, mmap, BF16/F64 correctos
  nx.H5Loader(path)                         lazy, float64 convertido
  nx.NpyLoader(path)                        lazy mmap para .npz, dir para .npy
  nx.NpzModelLoader(npz_path, model_key)    desde .npz sin archivo original

Deteccion de schema
  nx.detect_schema_gguf(path)               (schema_name, hp)
  nx.detect_schema_safetensors(path)        (schema_name, hp)
  nx.detect_schema_hf(path)                 (schema_name, hp)
  nx.detect_schema_npz(npz_path, key)       (schema_name, hp) desde .npz
  nx.detect_schema_from_tensors(names, ...) desde lista de nombres de tensores
  nx.list_models(npz_path)                  metadata de todos los modelos
  nx.SCHEMAS                                dict con todos los schemas

TensorRegistry
  nx.registry                               instancia global (hasta 3 modelos LRU)
  nx.TensorRegistry()                       instancia propia
  reg.register(name, loader, fmt)           indexa sin cargar datos
  reg.get(name, canonical)                  carga aqui — unico punto de I/O
  reg.has(name, canonical)                  O(1) sin cargar nada
  reg.list(name)                            lista de canonicos disponibles
  reg.release(name)                         liberar slot manualmente
  reg.stats()                               info de slots usados

Runtime LLM
  nx.ModelRunner(loader, schema_name, hp, max_seq=2048)      decoder-only
  nx.T5Runner(loader, schema_name, hp, max_seq=2048)         encoder-decoder (T5/Flan-T5/mT5/UL2)
    t5.encode(encoder_input_ids)                             una vez por secuencia
    t5.forward(decoder_input_ids)                            por paso, con KV cache
    t5.reset()                                                antes de una nueva secuencia
  nx.ActivationExtractor(runner, hooks=None, reduce=None, layer_fn=None, keep_copy=True)
    hooks:    ["embed","post_attn","residual_ffn","final_norm",...]
    reduce:   None | "last" | "mean"
    layer_fn: callable(i)->bool | list[int]
  nx.ActivationExtractor.extract(runner, input_ids, hooks, reduce)
  nx.ActivationExtractor.extract_bert(runner, input_ids, ...)
  nx.RoPECache / nx.KVCache / nx.WeightCache

Runtime CNN
  nx.CNNRunner(loader, n_blocks=13)                  MobileNetV1
  nx.InvertedResidualRunner(loader, n_blocks=17)     MobileNetV2 / V3
  nx.ResNetRunner(loader, depth=50, num_classes=1000)          18/34/50/101/152
  nx.EfficientNetRunner(loader, variant="b0", num_classes=1000) b0-b7 (V1)
  nx.ViTRunner(loader, variant="b_16", num_classes=1000, image_size=224)
  nx.CNNActivationExtractor(runner, reduce="spatial")
  nx.CNNActivationExtractor.extract(runner, image)
  nx.CanonicalLoader(raw_loader, mapped)

Canonico
  nx.map_tensors(tensors, fmt, strict=False)
  nx.find_unmapped(tensors, fmt, mapped)
  nx.resolve_tied_embeddings(mapped)
  nx.detect_format(tensors)
  nx.CanonicalTensor

Exportador
  nx.export_to_npz(src, dst, fmt=None, merge=False, verbose=True)
  nx.load_index(path)
  nx.inspect_npz(path, n=20)
  nx.save_activations(path, acts, model_key=None)
  nx.load_activations(path, model_key=None)

Ops — normalizacion
  nx.rmsnorm(x, weight, eps=1e-6)
  nx.layernorm(x, weight, bias=None, eps=1e-5)
  nx.batchnorm(x, gamma, beta, mean, var, eps=1e-3)
  nx.group_norm(x, num_groups, weight=None, bias=None)
  nx.instance_norm(x, weight=None, bias=None)

Ops — activaciones
  nx.softmax(x, axis=-1)
  nx.sigmoid(x)
  nx.relu(x)
  nx.leaky_relu(x, negative_slope=0.01)
  nx.elu(x, alpha=1.0)
  nx.prelu(x, weight)
  nx.gelu(x)
  nx.quick_gelu(x)
  nx.silu(x)
  nx.mish(x)
  nx.hardswish(x)

Ops — lineales
  nx.linear(x, weight, bias=None)
  nx.embedding(idx, weight)

Ops — FFN
  nx.swiglu(x, gate_w, up_w, down_w)
  nx.swiglu_fused(x, gate_up_w, down_w)
  nx.geglu(x, gate_w, up_w, down_w)
  nx.geglu_fused(x, gate_up_w, down_w)
  nx.ffn_gelu(x, up_w, down_w)
  nx.ffn_relu(x, up_w, down_w)

Ops — atencion
  nx.attention(q, k, v, n_heads, n_kv_heads, mask=None, scale=None)
    scale: None usa 1/sqrt(head_dim) (default, sin cambios); T5 pasa
    scale=1.0 (no escala, lo compensa via inicializacion de pesos)
  nx.scaled_dot_product(q, k, v, mask=None)
  nx.rope(x, pos=None, base=10000.0, positions=None)
  nx.t5_relative_position_bucket(relative_position, bidirectional=True, num_buckets=32, max_distance=128)
  nx.t5_compute_position_bias(rel_bias_table, q_len, k_len, bidirectional=True, ...)

Ops — CNN
  nx.conv2d(x, weight, bias=None, stride=1, padding=0)
  nx.depthwise_conv2d(x, weight, stride=1, padding=1)
  nx.global_avg_pool(x)
  nx.avg_pool2d(x, kernel_size=2, stride=None, padding=0)
  nx.max_pool2d(x, size=2, stride=2)
  nx.adaptive_avg_pool(x, output_size)
  nx.upsample_nearest(x, scale_factor=2)

---

## Correcciones v2

BF16 en SafeTensors — resultado silenciosamente incorrecto en v1.
  v1 convertia el valor numerico uint16 a uint32 antes de shiftear,
  lo cual es matematicamente incorrecto. v2 copia los bytes directamente
  a los bytes altos del float32, que es la definicion de BF16.
  Afectaba a: Mistral, LLaMA-3, Gemma, Phi y cualquier modelo HF en BF16.

Q8_1 en GGUF — scale ignorado en v1.
  v1 trataba los bytes int8 como float32 directos sin aplicar el scale d.
  v2 extrae d correctamente y devuelve d * qs.

RoPE — convencion incorrecta en v1.
  La segunda mitad usaba x1*sin + x2*cos en lugar de la convencion
  LLaMA canonica [-x2, x1]. Corregido en v2.

SwiGLU — usaba sigmoid(gate) en lugar de silu(gate).
  Silenciosamente incorrecto. silu(x) = x * sigmoid(x), no sigmoid(x).

conv2d / depthwise_conv2d — formato de pesos Keras vs PyTorch.
  v1 asumia (kH, kW, C_in, C_out). v2 usa formato PyTorch (C_out, C_in, kH, kW).

H5Loader — cargaba todos los arrays en el constructor (eager).
  Para un MobileNet eran ~100MB en RAM antes de pedir nada. v2 es lazy.

NpyLoader — cargaba el .npz completo en RAM en el constructor.
  v2 usa mmap_mode="r" — los arrays se leen del disco solo cuando se piden.

Exporter: se corrige defaul del archivo shora detcta mejor todo

---

## Correcciones y novedades v3

forward.py
  hf_t5 estaba declarado en SCHEMAS pero ModelRunner lo rechazaba con
  NotImplementedError ("requiere encoder+decoder"). Ahora nx.T5Runner
  lo ejecuta de verdad: encoder bidireccional + decoder con self-attn
  causal y cross-attn, bias posicional relativo por bucket logaritmico
  (verificado con 172 casos contra una traduccion literal del algoritmo
  de HuggingFace, y contra una implementacion de referencia en NumPy
  puro independiente del propio codigo de produccion). geglu_fused y
  ffn_relu de ops.py, que ya existian pero no estaban conectados al
  dispatcher de _ffn_layer, ahora se usan (T5 clasico usa un
  ffn_type nuevo, "gelu_no_gate", que es ReLU real sin puerta; T5v1.1/
  Flan-T5 reusan el "geglu" ya existente).

  sliding_window se detectaba desde hace tiempo en detect_schema_gguf/
  detect_schema_hf pero nunca se usaba en el forward -- Gemma 2/3 y
  Mistral corrian con atencion causal completa en silencio. Ahora
  _make_causal_mask acepta un ancho de ventana y ModelRunner.forward
  alterna capas locales/globales segun el patron real de cada
  arquitectura (Gemma 2: 1:1, Gemma 3: 5:1, Mistral: fijo en todas).

  Bug encontrado en la deteccion de arquitectura durante esta sesion:
  gemma3 se detectaba siempre como "gemma2" (mismo schema de tensores,
  pero patron de ventana distinto), y en la ruta sin config.json
  (detect_schema_safetensors) el orden de comprobaciones hacia que
  incluso cayera como "olmo2" por error (ambos tienen q_norm/k_norm).
  Corregido con una señal de tensor mas especifica.

  Bug adicional encontrado al ejecutar literalmente el ejemplo de uso
  de T5 de este mismo README (no detectado por ninguna de las pruebas
  anteriores de la sesion, que no ejercitaban este camino exacto):
  T5Runner.forward reusaba _resolve_lm_head, una funcion generica de
  ModelRunner que cae a schema["embed"] cuando no hay lm_head propio
  -- pero el schema de T5 no tiene una clave "embed" unica (tiene
  "enc_embed" y "dec_embed" por separado), asi que fallaba con
  KeyError('embed') incluso en el caso normal de tied_embeddings=True,
  que es el caso mas comun. Corregido con una resolucion de lm_head
  especifica dentro de T5Runner.forward. Verificado de nuevo tras el
  arreglo: consistencia batch/incremental identica a antes (diff
  1.27e-05), y los casos tied_embeddings=False y geglu (T5v1.1)
  siguen funcionando.

  "bart" apuntaba al mismo schema que T5 (hf_t5) pese a ser una
  arquitectura de forward distinta (posiciones absolutas + GELU, no
  bias relativo). Se retiro ese mapeo -- ver "Limitaciones conocidas".

  ALiBi (BLOOM) sigue sin implementar.

imag.py — CNN nuevas
  ResNetRunner (18/34/50/101/152), EfficientNetRunner (B0-B7, V1) y
  ViTRunner (B/16 a H/14) añadidos, formato de checkpoint torchvision.
  Antes solo MobileNet (v1/v2/v3) estaba cubierto pese a que las
  primitivas para estas arquitecturas (prelu, hardswish,
  scaled_dot_product) ya existian sueltas en ops.py sin ningun runner
  que las usara.

gguf.py
  Q8_K e IQ4_NL añadidos (tipos 15 y 20), verificados con pruebas que
  construyen el bloque binario byte a byte replicando el struct C real,
  no solo pasando arrays de NumPy a la propia funcion. IQ4_NL ademas
  verificado valor por valor contra su tabla de 16 codigos, confirmada
  con dos fuentes primarias independientes y coincidentes.

---

## Limitaciones conocidas (honestidad sobre lo que falta)

Estas son cosas identificadas durante el trabajo de esta sesion que
NO se implementaron, con la razon especifica de cada una -- no una
lista generica de "por hacer":

GGUF — tipos de cuantizacion sin implementar:
  IQ4_XS (tipo 23): usa la misma tabla de 16 valores que IQ4_NL, pero
    con escalas de 6 bits repartidas entre dos campos (scales_h de 2
    bits, scales_l de 4 bits) cuyo algoritmo exacto de combinacion no
    se pudo confirmar contra el codigo fuente real.
  IQ2_XXS/XS/S, IQ3_XXS/S, IQ1_S/M: requieren tablas de codebook de
    cientos o miles de valores (grid lattice E8, inicializadas en
    tiempo de ejecucion en el C real) que no se pudieron extraer y
    verificar con la misma rigurosidad que una tabla pequeña de 16
    valores. El riesgo de un error silencioso de transcripcion en una
    tabla de esa envergadura es alto.
  TQ1_0/TQ2_0 (tipos 34/35, ternarizacion): IDs de tipo confirmados,
    pero el struct de bloque exacto (empaquetamiento base-3) no se
    pudo verificar con certeza suficiente.

forward.py:
  BART: sigue sin soporte real (ver arriba). Un checkpoint BART hoy
    falla con KeyError en vez de correr con matematica incorrecta,
    pero no es soporte real.
  ALiBi (BLOOM): no implementado.

imag.py:
  EfficientNetV2: usa FusedMBConv en sus primeras etapas, una segunda
    familia de bloques con estructura de tensor distinta de V1 -- no
    cubierto.
  El puerto de HuggingFace de EfficientNet (nombres de tensor propios,
    distintos de torchvision) no esta cubierto; solo torchvision.
  EfficientNetRunner se valido con pesos sinteticos de shape correcta,
    no contra un checkpoint real descargado (sin acceso a red en el
    entorno de desarrollo) -- la validacion es estructural, no una
    comparacion numerica directa contra torchvision real.

ML clasico (arboles de decision, ensambles, modelos lineales, SVM):
  Discutido en esta sesion pero NO implementado. Queda como trabajo
  futuro con el formato de entrada (pickle/joblib vs un formato propio
  mas seguro) y el orden de prioridad (arboles vs modelos lineales)
  por decidir.

Archivos existentes con incoherencias encontradas durante la revision
de esta sesion, sin modificar (fuera del alcance acordado):
  micronnx/loaders/exporter.py (50 lineas) parece un archivo huerfano:
    su docstring dice "micronnx.executor -- ops, forward pass...",
    reexporta simbolos de micronnx.executor.*, pero vive en
    micronnx/loaders/ y micronnx/loaders/__init__.py no lo importa en
    absoluto (solo importa safetensors, gguf, h5, npy). Nada en el
    proyecto parece importar desde micronnx.loaders.exporter. Es
    candidato a ser basura de un refactor anterior sin limpiar, pero
    no se elimino ni modifico porque no se confirmo con certeza que
    sea seguro hacerlo sin mas contexto del historial del proyecto.
  micronnx/executor/canonical.py y micronnx/executor/npz_runner.py
    (el sistema de mapeo canonico usado por export_to_npz, paralelo al
    SCHEMAS/detect_schema_* de forward.py) no tienen ningun concepto
    de sliding_window ni de la deteccion diferenciada gemma2/gemma3
    añadida en esta sesion. Un modelo exportado a .npz y recargado via
    NpzModelLoader hoy NO llevaria consigo el sliding window real,
    aunque cargado directamente desde el .gguf/.safetensors original
    si funciona correctamente. Tampoco tienen conocimiento de T5Runner
    (canonical.py si tiene reglas de mapeo de nombres para T5, de
    antes de esta sesion, y coinciden con los nombres usados en
    T5Runner -- pero npz_runner.py no sabe instanciar T5Runner en vez
    de ModelRunner para un modelo con schema hf_t5).
  micronnx/executor/exporter.py (el exportador real, 638 lineas) no
    tiene ninguna referencia a T5Runner ni a sliding_window -- no se
    confirmo si el proceso de exportacion a .npz preserva estos datos
    nuevos correctamente para los modelos que los usan.

---

## Dependencias

  numpy >= 1.24
  pyfive  (para archivos .h5 / .keras)

---

## Parte del ecosistema UFM

micronnx es la capa de recoleccion de UFM (Unified Fusion Model).
Su unica responsabilidad es exponer pesos y activaciones en una estructura uniforme.
La logica de fusion, compatibilidad y ajuste fino vive en UFM, no aqui.

---

## Licencia

MIT
