Metadata-Version: 2.4
Name: micronnx
Version: 0.1.2.3
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_0, Q8_1, 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

---

## Formatos soportados

GGUF (.gguf)
  Q2_K, Q3_K, Q4_0, Q4_1, Q4_K, Q5_0, Q5_1, Q5_K, Q6_K
  Q8_0, Q8_1 (corregido en v2), BF16, F16, F32

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_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

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, Flan-T5, MobileNetV1, MobileNetV2, MobileNetV3

---

## 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)

---

## 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)
  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.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)
  nx.scaled_dot_product(q, k, v, mask=None)
  nx.rope(x, pos=None, base=10000.0, positions=None)

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
---

## 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
