--- license: cc-by-nc-4.0 library_name: mlx pipeline_tag: token-classification base_model: feyninc/pulpie-orange-small tags: - mlx - eurobert - token-classification - html - content-extraction - boilerplate-removal - web-scraping - encoder - custom-code --- # Pulpie Orange Small MLX This repository contains MLX weights for [`feyninc/pulpie-orange-small`](https://huggingface.co/feyninc/pulpie-orange-small), a 210M-parameter EuroBERT token-classification model for main-content extraction from HTML. The source checkpoint is an encoder-only EuroBERT model with RoPE, RMSNorm, SwiGLU MLP layers, and a token-classification head. It is not a decoder-only LLM, so this conversion does not use `mlx-lm`'s standard LLM model classes. Instead, this repository includes `modeling_eurobert_mlx.py`, a small MLX implementation of the source architecture with matching parameter names. ## Files | File | Purpose | | --- | --- | | `model-bf16.safetensors` | Native 16-bit BF16 MLX weights converted from the source checkpoint. | | `model-8bit.safetensors` | MLX affine 8-bit weight-quantized variant. | | `model-4bit.safetensors` | MLX affine 4-bit weight-quantized variant. | | `modeling_eurobert_mlx.py` | MLX EuroBERT token-classification loader. | | `mlx_config.json` | Variant metadata and quantization settings. | | `verification_report.json` | Load, numerical, extraction, latency, and compute-cost results. | | `scripts/convert_to_mlx.py` | Reproducible conversion script. | | `scripts/verify_mlx.py` | Reproducible verification script. | ## Usage Install dependencies: ```bash pip install -r requirements.txt ``` Run a forward pass: ```python import sys import mlx.core as mx from huggingface_hub import snapshot_download from transformers import AutoTokenizer repo_dir = snapshot_download("Mike0021/pulpie-orange-small-mlx") sys.path.insert(0, repo_dir) from modeling_eurobert_mlx import load_model tokenizer = AutoTokenizer.from_pretrained(repo_dir, trust_remote_code=True) model = load_model(repo_dir, variant="bf16") # "bf16", "8bit", or "4bit" inputs = tokenizer( ["

Apple MLX conversion

Main article text.

"], return_tensors="np", padding=True, ) input_ids = mx.array(inputs["input_ids"]) attention_mask = mx.array(inputs["attention_mask"]) logits = model(input_ids, attention_mask) mx.eval(logits) print(logits.shape) # batch, tokens, 2 ``` Minimal Pulpie-style extraction: ```python import numpy as np import mlx.core as mx from pulpie.chunker import extract_blocks, pack_chunks, tokenize_blocks from pulpie.model_utils import extract_item_ids, predictions_to_labels from pulpie.reconstruct import extract_main_html from pulpie.simplify import simplify html = ( "

Apple MLX conversion

" "

This article explains how to convert a EuroBERT content extraction " "model to MLX format.

" ) simplified, map_html = simplify(html) blocks = extract_blocks(simplified) item_ids = extract_item_ids(blocks) sep_id = tokenizer.convert_tokens_to_ids("<|sep|>") chunks = pack_chunks( tokenize_blocks(blocks, tokenizer), max_tokens=8192, sep_token_id=sep_id, bos_token_id=tokenizer.bos_token_id, eos_token_id=tokenizer.eos_token_id, ) predictions = [0] * len(blocks) for chunk_ids, block_indices in chunks: ids = mx.array([chunk_ids]) mask = mx.ones_like(ids) logits = model(ids, mask) mx.eval(logits) logits_np = np.array(logits.astype(mx.float32))[0] sep_positions = np.where(np.array(chunk_ids) == sep_id)[0] preds = logits_np[sep_positions].argmax(axis=-1).tolist() for i, block_idx in enumerate(block_indices): predictions[block_idx] = int(preds[i]) labels = predictions_to_labels(item_ids, predictions) print(extract_main_html(map_html, labels)) ``` ## Conversion Methodology 1. Downloaded `feyninc/pulpie-orange-small` from the Hugging Face Hub. 2. Inspected `config.json`, `configuration_eurobert.py`, and `modeling_eurobert.py`. 3. Confirmed the model is `EuroBertForTokenClassification` with 12 layers, hidden size 768, 12 attention heads, head dim 64, max length 8192, BF16 source weights, and 2 output labels. 4. Confirmed current MLX can be installed on Linux with `mlx[cpu]`, so no cloud Mac was required for conversion or load verification. 5. Implemented a custom MLX EuroBERT token-classification module with matching state-dict keys and the source architecture behavior. 6. Saved the BF16 MLX weights with `mlx.core.save_safetensors`. 7. Created 8-bit and 4-bit variants with `mlx.nn.quantize`, using affine weight quantization, group size 64, over MLX `Linear` and `Embedding` modules. 8. Verified each variant with `scripts/verify_mlx.py`. ## Verification Results Verification was run on Linux x86_64 using `mlx[cpu]`. The PyTorch reference was the original source checkpoint loaded in float32 with eager attention. The BF16 variant is expected to have small dtype-level differences versus that float32 reference; quantized variants have larger differences. ### Load Checks | Variant | Load result | Test logits shape | Test logits dtype | | --- | --- | --- | --- | | BF16 | Pass | `[1, 3, 2]` | `mlx.core.bfloat16` | | 8-bit | Pass | `[1, 3, 2]` | `mlx.core.bfloat16` | | 4-bit | Pass | `[1, 3, 2]` | `mlx.core.bfloat16` | ### Numerical Accuracy Test inputs: `["A", "B", "C"]`, token shape `[3, 2]`. | Variant | Max abs diff vs PyTorch fp32 | Mean abs diff | MLX CPU latency | | --- | ---: | ---: | ---: | | BF16 | 0.0452327728 | 0.0191817340 | 716.88 ms | | 8-bit | 1.2797489166 | 0.5423613191 | 9380.58 ms | | 4-bit | 2.2551989555 | 1.1897996664 | 9323.18 ms | PyTorch fp32 eager latency on the same input was 50.09 ms on this Linux CPU. The quantized MLX CPU path is slow on this host and should not be read as an Apple Silicon benchmark. ### End-to-End Extraction Sample HTML: ```html

Apple MLX conversion

This article explains how to convert a EuroBERT content extraction model to MLX format.

``` Pulpie preprocessing produced 2 blocks and one 50-token chunk. All variants loaded, classified both blocks as `main`, and reconstructed non-empty HTML. | Variant | Predictions | Non-empty output | MLX CPU extraction latency | | --- | --- | --- | ---: | | BF16 | `[1, 1]` | Pass | 5469.05 ms | | 8-bit | `[1, 1]` | Pass | 78333.61 ms | | 4-bit | `[1, 1]` | Pass | 77852.02 ms | Full machine-readable results are in `verification_report.json`. ## Limitations - This is a custom MLX encoder/token-classification implementation, not an `mlx-lm` decoder model. - The 8-bit and 4-bit variants are weight-only affine MLX quantizations. They load and pass a small extraction test, but full WebMainBench quality was not re-evaluated. - Linux CPU quantized latency is poor in this environment. MLX is primarily intended for Apple Silicon GPU execution. - The source tokenizer currently emits a Transformers regex warning. The verifier keeps the tokenizer behavior used by the published `pulpie` package rather than changing token IDs during conversion. ## Compute Cost No paid cloud Mac or hosted GPU was used. Conversion and verification were done locally on Linux x86_64 with the MLX CPU package. Incremental compute cost: `$0.00`. ## License The source model weights are licensed under CC BY-NC 4.0. This converted checkpoint follows the same non-commercial license. The included conversion and loader code is provided for interoperability with the converted weights.