Instructions to use ioxdue/moediet-runs with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use ioxdue/moediet-runs with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf ioxdue/moediet-runs:Q4_K_M # Run inference directly in the terminal: llama cli -hf ioxdue/moediet-runs:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf ioxdue/moediet-runs:Q4_K_M # Run inference directly in the terminal: llama cli -hf ioxdue/moediet-runs:Q4_K_M
Build from source code
git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-app # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama serve -hf ioxdue/moediet-runs:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama cli -hf ioxdue/moediet-runs:Q4_K_M
Use Docker
docker model run hf.co/ioxdue/moediet-runs:Q4_K_M
- Llama new
- LM Studio
- Jan
- Ollama
How to use ioxdue/moediet-runs with Ollama:
ollama run hf.co/ioxdue/moediet-runs:Q4_K_M
- Unsloth Desktop
- Docker Model Runner
How to use ioxdue/moediet-runs with Docker Model Runner:
docker model run hf.co/ioxdue/moediet-runs:Q4_K_M
- Lemonade
How to use ioxdue/moediet-runs with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull ioxdue/moediet-runs:Q4_K_M
Run and chat with the model
lemonade run user.moediet-runs-Q4_K_M
List all available models
lemonade list
- Atomic Chat
MoEDiet: codice e parametri del Job
Browse files- requests/4af9bd4213d54162b4aec991fb2a55a7/fidelity.py +69 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/gguf_export.py +131 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/hf_jobs.py +88 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/llama_tools.py +95 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/moediet.py +303 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/probes.py +90 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/request.json +1 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/requirements-q4.txt +6 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/requirements.txt +4 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/routing.py +129 -0
- requests/4af9bd4213d54162b4aec991fb2a55a7/run.py +144 -0
requests/4af9bd4213d54162b4aec991fb2a55a7/fidelity.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare next-token distributions on held-out, original-model greedy continuations."""
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from routing import continuation, encode
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class Trace:
|
| 12 |
+
group: str
|
| 13 |
+
prefix: torch.Tensor # Input through the penultimate token, on CPU.
|
| 14 |
+
original: torch.Tensor # Log probabilities for the generated positions, on CPU.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@torch.inference_mode()
|
| 18 |
+
def log_probs(model, prefix, steps):
|
| 19 |
+
out = model(input_ids=prefix.to(model.get_input_embeddings().weight.device), use_cache=False,
|
| 20 |
+
output_router_logits=False, logits_to_keep=steps, return_dict=True)
|
| 21 |
+
logp = out.logits[0].float().log_softmax(-1).cpu()
|
| 22 |
+
if logp.shape[0] != steps or not torch.isfinite(logp).all():
|
| 23 |
+
raise ValueError('Log-probabilità mancanti o non finite.')
|
| 24 |
+
return logp
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@torch.inference_mode()
|
| 28 |
+
def make_traces(model, tokenizer, groups, steps):
|
| 29 |
+
traces = []
|
| 30 |
+
for group, prompts in groups.items():
|
| 31 |
+
for prompt in prompts:
|
| 32 |
+
ids = encode(model, tokenizer, prompt, reserve=steps)
|
| 33 |
+
sequence = continuation(model, tokenizer, ids, steps)
|
| 34 |
+
used = sequence.shape[1] - ids.shape[1] # EOS may shorten the reference.
|
| 35 |
+
if used < 1:
|
| 36 |
+
raise ValueError('Il modello di riferimento ha generato una continuazione vuota.')
|
| 37 |
+
prefix = sequence[:, :-1].cpu()
|
| 38 |
+
traces.append(Trace(group, prefix, log_probs(model, prefix, used)))
|
| 39 |
+
return traces
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def replay(model, traces):
|
| 43 |
+
return [log_probs(model, trace.prefix, len(trace.original)) for trace in traces]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def compare(traces, candidate, reference=None):
|
| 47 |
+
if reference is None:
|
| 48 |
+
reference = [t.original for t in traces]
|
| 49 |
+
totals = {}
|
| 50 |
+
for trace, base, new in zip(traces, reference, candidate, strict=True):
|
| 51 |
+
# Sum over vocab then positions; per-group means use the number of generated positions.
|
| 52 |
+
base, new = base.double().log_softmax(-1), new.double().log_softmax(-1)
|
| 53 |
+
difference = base - new
|
| 54 |
+
delta = (base.exp() * difference).sum().item()
|
| 55 |
+
hits = (base.argmax(-1) == new.argmax(-1)).sum().item()
|
| 56 |
+
row = totals.setdefault(trace.group, {'prompts': 0, 'tokens': 0, 'kl_sum': 0.0, 'hits': 0, 'peak': 0.0})
|
| 57 |
+
row['prompts'] += 1
|
| 58 |
+
row['tokens'] += len(base)
|
| 59 |
+
row['kl_sum'] += delta
|
| 60 |
+
row['hits'] += hits
|
| 61 |
+
row['peak'] = max(row['peak'], difference.abs().max().item())
|
| 62 |
+
return {name: {'prompts': row['prompts'], 'tokens': row['tokens'],
|
| 63 |
+
'kl_nats_per_token': max(0.0, row['kl_sum'] / row['tokens']),
|
| 64 |
+
'top1_agreement': row['hits'] / row['tokens'],
|
| 65 |
+
'max_abs_logp_delta': row['peak']} for name, row in totals.items()}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def passes(metrics, limit):
|
| 69 |
+
return bool(metrics) and all(m['kl_nats_per_token'] <= limit for m in metrics.values())
|
requests/4af9bd4213d54162b4aec991fb2a55a7/gguf_export.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export an accepted MoEDiet run to a quantized GGUF using native llama.cpp."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from collections import Counter
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import re
|
| 9 |
+
import subprocess
|
| 10 |
+
import sys
|
| 11 |
+
import tempfile
|
| 12 |
+
|
| 13 |
+
from llama_tools import get_tools, sha256
|
| 14 |
+
|
| 15 |
+
QTYPES = ('Q4_K_M', 'Q4_0')
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def model_config(run, report):
|
| 19 |
+
cfg = json.loads((run / 'checkpoint' / 'config.json').read_text(encoding='utf-8'))
|
| 20 |
+
native = report['routing']
|
| 21 |
+
if (not report.get('checkpoint') or not report.get('checkpoint_audit', {}).get('passed')
|
| 22 |
+
or cfg.get('model_type') != 'olmoe' or cfg['num_experts'] != report['retained_per_layer']
|
| 23 |
+
or cfg['num_experts_per_tok'] != native['experts_per_token']
|
| 24 |
+
or cfg['num_hidden_layers'] != native['num_layers']
|
| 25 |
+
or not cfg['num_experts_per_tok'] <= cfg['num_experts'] < native['experts_per_layer']):
|
| 26 |
+
raise ValueError('Il checkpoint non corrisponde al taglio accettato nel report.')
|
| 27 |
+
# The pinned native OLMoE graph has these semantics. Refuse variants it would silently change.
|
| 28 |
+
if (cfg.get('norm_topk_prob', False) or cfg.get('clip_qkv') is not None or cfg.get('attention_bias', False)
|
| 29 |
+
or cfg.get('rope_scaling') or cfg.get('hidden_act', 'silu') != 'silu'
|
| 30 |
+
or cfg.get('rms_norm_eps', 1e-5) != 1e-5
|
| 31 |
+
or cfg.get('num_key_value_heads', cfg['num_attention_heads']) != cfg['num_attention_heads']):
|
| 32 |
+
raise ValueError('Configurazione OLMoE fuori dal profilo GGUF verificato (AllenAI 0125).')
|
| 33 |
+
return cfg
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def check_gguf(file, source, cfg, qtype):
|
| 37 |
+
sys.path.insert(0, str(source / 'gguf-py'))
|
| 38 |
+
try:
|
| 39 |
+
from gguf import GGUFReader, LlamaFileType
|
| 40 |
+
finally:
|
| 41 |
+
sys.path.pop(0)
|
| 42 |
+
reader = GGUFReader(str(file))
|
| 43 |
+
expected = {'general.architecture': 'olmoe', 'general.file_type': int(getattr(LlamaFileType, 'MOSTLY_' + qtype)),
|
| 44 |
+
'olmoe.block_count': cfg['num_hidden_layers'], 'olmoe.expert_count': cfg['num_experts'],
|
| 45 |
+
'olmoe.expert_used_count': cfg['num_experts_per_tok']}
|
| 46 |
+
for key, value in expected.items():
|
| 47 |
+
if key not in reader.fields or reader.fields[key].contents() != value:
|
| 48 |
+
raise ValueError(f'Metadato GGUF errato o mancante: {key}')
|
| 49 |
+
names = {f'blk.{i}.ffn_{axis}_exps.weight' for i in range(cfg['num_hidden_layers'])
|
| 50 |
+
for axis in ('gate', 'up', 'down')}
|
| 51 |
+
experts = {tensor.name: tensor for tensor in reader.tensors if '_exps.weight' in tensor.name}
|
| 52 |
+
if set(experts) != names or any(len(t.shape) != 3 or t.shape[2] != cfg['num_experts'] for t in experts.values()):
|
| 53 |
+
raise ValueError('Tensori degli esperti mancanti o dimensione errata nel GGUF.')
|
| 54 |
+
counts = Counter(t.tensor_type.name for t in experts.values())
|
| 55 |
+
if counts['Q4_K' if qtype == 'Q4_K_M' else 'Q4_0'] == 0:
|
| 56 |
+
raise ValueError(f'Nessuna matrice degli esperti quantizzata realmente in {qtype}.')
|
| 57 |
+
return dict(counts)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def export_run(run, qtype='Q4_K_M', llama_dir=None, bin_dir=None):
|
| 61 |
+
run = Path(os.path.expandvars(str(run))).expanduser().resolve()
|
| 62 |
+
if llama_dir:
|
| 63 |
+
llama_dir = Path(os.path.expandvars(str(llama_dir))).expanduser()
|
| 64 |
+
if bin_dir:
|
| 65 |
+
bin_dir = Path(os.path.expandvars(str(bin_dir))).expanduser()
|
| 66 |
+
if qtype not in QTYPES:
|
| 67 |
+
raise ValueError(f'Quantizzazione sconosciuta: {qtype}')
|
| 68 |
+
report = json.loads((run / 'report.json').read_text(encoding='utf-8'))
|
| 69 |
+
if report['status'] != 'pruned':
|
| 70 |
+
if report['status'] not in ('mask_only', 'mask_only_checkpoint_audit_failed',
|
| 71 |
+
'unchanged_audit_failed', 'unchanged_no_candidate'):
|
| 72 |
+
raise ValueError(f"Stato della build sconosciuto: {report['status']}")
|
| 73 |
+
print(f"GGUF saltato: {report['status']}. Nessun checkpoint fisico accettato.", flush=True)
|
| 74 |
+
return None
|
| 75 |
+
cfg = model_config(run, report)
|
| 76 |
+
dest = run / 'gguf' / qtype
|
| 77 |
+
if dest.exists():
|
| 78 |
+
raise ValueError(f'Esiste già un export {qtype}: {dest}')
|
| 79 |
+
tools = get_tools(llama_dir, bin_dir)
|
| 80 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
with tempfile.TemporaryDirectory(prefix='.convert-', dir=dest.parent) as tmp:
|
| 82 |
+
work = Path(tmp)
|
| 83 |
+
stage = work / 'publish'
|
| 84 |
+
stage.mkdir()
|
| 85 |
+
fp16, result = work / 'intermediate-f16.gguf', stage / f'model-{qtype}.gguf'
|
| 86 |
+
env = os.environ.copy()
|
| 87 |
+
env.pop('NO_LOCAL_GGUF', None)
|
| 88 |
+
print(f'Conversione del checkpoint in F16, poi {qtype}...', flush=True)
|
| 89 |
+
subprocess.run([sys.executable, str(tools.source / 'convert_hf_to_gguf.py'), str(run / 'checkpoint'),
|
| 90 |
+
'--outtype', 'f16', '--outfile', str(fp16)], env=env, check=True)
|
| 91 |
+
subprocess.run([str(tools.quantize), str(fp16), str(result), qtype], check=True)
|
| 92 |
+
counts = check_gguf(result, tools.source, cfg, qtype)
|
| 93 |
+
smoke_cmd = [str(tools.cli), '-m', str(result), '-ngl', '0',
|
| 94 |
+
'--no-warmup', '--no-display-prompt', '--check-tensors', '--ignore-eos', '-c', '64',
|
| 95 |
+
'-t', '2', '-n', '2', '--temp', '0', '-p', 'print(1)']
|
| 96 |
+
smoke = subprocess.run(smoke_cmd, stdin=subprocess.DEVNULL, capture_output=True, timeout=600)
|
| 97 |
+
logs = smoke.stderr.decode('utf-8', errors='replace')
|
| 98 |
+
out = smoke.stdout.decode('utf-8', errors='replace')
|
| 99 |
+
prefill = re.search(r'prompt eval time\s*=[^\n/]+/\s*([1-9]\d*)\s+tokens?', logs)
|
| 100 |
+
decode = re.search(r':\s*eval time\s*=[^\n/]+/\s*([1-9]\d*)\s+(?:runs?|tokens?)', logs)
|
| 101 |
+
if not prefill or not decode:
|
| 102 |
+
m = re.search(r'Prompt:\s*([0-9.]+)\s*t/s\s*\|\s*Generation:\s*([0-9.]+)\s*t/s', out)
|
| 103 |
+
if m:
|
| 104 |
+
prefill = (None, '2')
|
| 105 |
+
decode = (None, '2')
|
| 106 |
+
if smoke.returncode or not prefill or not decode:
|
| 107 |
+
raise ValueError('Smoke test llama-cli fallito.\n' + (logs + '\n' + out)[-2200:])
|
| 108 |
+
record = {'file': result.name, 'quantization': qtype, 'bytes': result.stat().st_size,
|
| 109 |
+
'sha256': sha256(result), 'llama_cpp_revision': tools.revision,
|
| 110 |
+
'layers': cfg['num_hidden_layers'], 'experts_per_layer': cfg['num_experts'],
|
| 111 |
+
'experts_per_token': cfg['num_experts_per_tok'], 'expert_tensor_types': counts,
|
| 112 |
+
'smoke': {'backend': 'llama.cpp/CPU', 'requested_tokens': 2, 'passed': True,
|
| 113 |
+
'prompt_tokens_evaluated': int(prefill[1]), 'decode_steps': int(decode[1])},
|
| 114 |
+
'post_quantization_task_quality_measured': False}
|
| 115 |
+
(stage / 'conversion.json').write_text(json.dumps(record, indent=2) + '\n', encoding='utf-8')
|
| 116 |
+
stage.rename(dest)
|
| 117 |
+
print(f'GGUF verificato: {dest / result.name}', flush=True)
|
| 118 |
+
return dest
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
if __name__ == '__main__':
|
| 122 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 123 |
+
parser.add_argument('--run', required=True, type=Path)
|
| 124 |
+
parser.add_argument('--qtype', choices=QTYPES, default=QTYPES[0])
|
| 125 |
+
parser.add_argument('--llama-dir', type=Path)
|
| 126 |
+
parser.add_argument('--bin-dir', type=Path)
|
| 127 |
+
args = parser.parse_args()
|
| 128 |
+
try:
|
| 129 |
+
export_run(args.run, args.qtype, args.llama_dir, args.bin_dir)
|
| 130 |
+
except (ValueError, OSError, ImportError, subprocess.SubprocessError) as error:
|
| 131 |
+
sys.exit(f'Errore: {error}')
|
requests/4af9bd4213d54162b4aec991fb2a55a7/hf_jobs.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Private, revision-pinned code submission and durable outputs for HF Jobs."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import shutil
|
| 7 |
+
import tempfile
|
| 8 |
+
from uuid import uuid4
|
| 9 |
+
|
| 10 |
+
from huggingface_hub import HfApi, get_token
|
| 11 |
+
|
| 12 |
+
HERE = Path(__file__).resolve().parent
|
| 13 |
+
IMAGE = 'pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel@sha256:a7103283ea7113e10ae5d014bd2342acebda0bc53164b2f7b1dd6eb7a766bdb6'
|
| 14 |
+
SOURCES = ('moediet.py', 'probes.py', 'routing.py', 'fidelity.py', 'gguf_export.py', 'llama_tools.py',
|
| 15 |
+
'run.py', 'hf_jobs.py', 'requirements.txt', 'requirements-q4.txt')
|
| 16 |
+
# No model, token, local commands, or user-supplied values are interpolated into this program.
|
| 17 |
+
BOOTSTRAP = '''
|
| 18 |
+
import os, sys, subprocess, tempfile
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
subprocess.run([sys.executable, '-m', 'pip', 'install', 'huggingface_hub==0.36.2'], check=True)
|
| 21 |
+
from huggingface_hub import snapshot_download
|
| 22 |
+
repo, prefix = os.environ['MOEDIET_REPO'], os.environ['MOEDIET_CODE']
|
| 23 |
+
with tempfile.TemporaryDirectory() as job:
|
| 24 |
+
snapshot_download(repo, revision=os.environ['MOEDIET_COMMIT'], allow_patterns=[prefix + '/*'],
|
| 25 |
+
local_dir=job, token=os.environ['HF_TOKEN'])
|
| 26 |
+
code = Path(job) / prefix
|
| 27 |
+
import json
|
| 28 |
+
cfg = json.loads((code / 'request.json').read_text())
|
| 29 |
+
deps = ['-r', str(code / 'requirements-q4.txt'), 'cmake>=3.28,<4'] if cfg['quantization'] else ['-r', str(code / 'requirements.txt')]
|
| 30 |
+
subprocess.run([sys.executable, '-m', 'pip', 'install', *deps], check=True)
|
| 31 |
+
subprocess.run([sys.executable, str(code / 'hf_jobs.py')], cwd=job, check=True)
|
| 32 |
+
'''
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def submit(cfg):
|
| 36 |
+
token = get_token()
|
| 37 |
+
if not token:
|
| 38 |
+
raise ValueError('Autenticarsi con hf auth login oppure impostare HF_TOKEN.')
|
| 39 |
+
api = HfApi(token=token)
|
| 40 |
+
repo = cfg['output_repo']
|
| 41 |
+
# Check access and resolve the input before submitting billable compute.
|
| 42 |
+
source = api.model_info(cfg['model'], revision=cfg['build'].get('revision'))
|
| 43 |
+
if not source.sha:
|
| 44 |
+
raise ValueError('Commit del modello sorgente non disponibile.')
|
| 45 |
+
api.create_repo(repo_id=repo, private=True, exist_ok=True)
|
| 46 |
+
if api.model_info(repo).private is not True:
|
| 47 |
+
raise ValueError('La destinazione esiste ed è pubblica. Scegliere un repository privato.')
|
| 48 |
+
prefix = 'requests/' + uuid4().hex
|
| 49 |
+
with tempfile.TemporaryDirectory(prefix='moediet-job-') as tmp:
|
| 50 |
+
stage = Path(tmp)
|
| 51 |
+
for name in SOURCES:
|
| 52 |
+
shutil.copyfile(HERE / name, stage / name)
|
| 53 |
+
payload = {'model': cfg['model'], 'tags': cfg['tags'], 'quantization': cfg['quantization'],
|
| 54 |
+
'build': {**cfg['build'], 'revision': source.sha}}
|
| 55 |
+
if cfg.get('corpus'):
|
| 56 |
+
shutil.copyfile(cfg['corpus'], stage / 'corpus.json')
|
| 57 |
+
payload['corpus'] = 'corpus.json'
|
| 58 |
+
(stage / 'request.json').write_text(json.dumps(payload, ensure_ascii=False), encoding='utf-8')
|
| 59 |
+
commit = api.upload_folder(repo_id=repo, folder_path=stage, path_in_repo=prefix,
|
| 60 |
+
commit_message='MoEDiet: codice e parametri del Job')
|
| 61 |
+
job = api.run_job(image=IMAGE, command=['python', '-c', BOOTSTRAP], flavor=cfg['flavor'],
|
| 62 |
+
timeout=cfg['timeout'], secrets={'HF_TOKEN': token},
|
| 63 |
+
env={'MOEDIET_REPO': repo, 'MOEDIET_CODE': prefix, 'MOEDIET_COMMIT': commit.oid})
|
| 64 |
+
print(f'Job: {job.url}\nOutput dopo il salvataggio: https://huggingface.co/{repo}/tree/main/runs/{job.id}')
|
| 65 |
+
return job
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def worker():
|
| 69 |
+
from run import execute
|
| 70 |
+
cfg = json.loads((HERE / 'request.json').read_text(encoding='utf-8'))
|
| 71 |
+
if 'corpus' in cfg:
|
| 72 |
+
cfg['corpus'] = str(HERE / 'corpus.json')
|
| 73 |
+
repo = os.environ['MOEDIET_REPO']
|
| 74 |
+
api = HfApi(token=os.environ['HF_TOKEN'])
|
| 75 |
+
prefix = 'runs/' + os.environ['JOB_ID']
|
| 76 |
+
|
| 77 |
+
def persist(folder, suffix):
|
| 78 |
+
remote_path = prefix + ('/' + suffix if suffix else '')
|
| 79 |
+
commit = api.upload_folder(repo_id=repo, folder_path=folder, path_in_repo=remote_path,
|
| 80 |
+
commit_message=f'MoEDiet: {remote_path}')
|
| 81 |
+
print(f'Salvato: https://huggingface.co/{repo}/tree/{commit.oid}/{remote_path}', flush=True)
|
| 82 |
+
|
| 83 |
+
with tempfile.TemporaryDirectory(prefix='moediet-result-') as tmp:
|
| 84 |
+
execute(cfg, Path(tmp) / 'result', publish=persist)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
if __name__ == '__main__':
|
| 88 |
+
worker()
|
requests/4af9bd4213d54162b4aec991fb2a55a7/llama_tools.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pinned CPU tools for GGUF conversion and validation, also on Windows ARM64."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import os
|
| 5 |
+
import platform
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
import tarfile
|
| 9 |
+
import tempfile
|
| 10 |
+
import urllib.request
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
COMMIT = 'a7a98e0fffed794396b3fbad4dcdbbc184963645' # llama.cpp b6500
|
| 15 |
+
ARCHIVE_SHA256 = '4d78e6aa4a9124b58dff994416525294fbab2990fe905a640d4cbd26bf563a31'
|
| 16 |
+
TARGETS = ('llama-quantize', 'llama-cli')
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def sha256(path):
|
| 20 |
+
digest = hashlib.sha256()
|
| 21 |
+
with path.open('rb') as stream:
|
| 22 |
+
while block := stream.read(4 << 20):
|
| 23 |
+
digest.update(block)
|
| 24 |
+
return digest.hexdigest()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass(frozen=True)
|
| 28 |
+
class Tools:
|
| 29 |
+
source: Path
|
| 30 |
+
quantize: Path
|
| 31 |
+
cli: Path
|
| 32 |
+
revision: str
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def installed(source, binary_dir=None, revision='user-supplied'):
|
| 36 |
+
source = Path(os.path.expandvars(str(source))).expanduser().resolve()
|
| 37 |
+
if not (source / 'convert_hf_to_gguf.py').is_file() or not (source / 'gguf-py' / 'gguf').is_dir():
|
| 38 |
+
raise ValueError(f'Checkout llama.cpp incompleto: {source}')
|
| 39 |
+
dirs = [Path(os.path.expandvars(str(binary_dir))).expanduser().resolve()] if binary_dir else [
|
| 40 |
+
source / 'build/bin/Release', source / 'build/bin', source / 'bin']
|
| 41 |
+
suffix = '.exe' if os.name == 'nt' else ''
|
| 42 |
+
for directory in dirs:
|
| 43 |
+
bins = [directory / (name + suffix) for name in TARGETS]
|
| 44 |
+
if all(file.is_file() for file in bins):
|
| 45 |
+
return Tools(source, *bins, revision)
|
| 46 |
+
raise ValueError('Servono llama-quantize e llama-cli della stessa build. Verificare bin_dir.')
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_tools(source=None, binary_dir=None):
|
| 50 |
+
if source:
|
| 51 |
+
return installed(source, binary_dir)
|
| 52 |
+
if binary_dir:
|
| 53 |
+
raise ValueError('bin_dir richiede llama_dir.')
|
| 54 |
+
tools_cache = os.environ.get('MOEDIET_TOOLS_CACHE')
|
| 55 |
+
if tools_cache:
|
| 56 |
+
root = Path(os.path.expandvars(str(tools_cache))).expanduser().resolve()
|
| 57 |
+
else:
|
| 58 |
+
root = (Path.home() / '.cache' / 'moediet').resolve()
|
| 59 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 60 |
+
target = root / f'llama-{COMMIT[:12]}-{platform.system()}-{platform.machine()}'
|
| 61 |
+
if target.exists():
|
| 62 |
+
return installed(target, revision=COMMIT)
|
| 63 |
+
if not shutil.which('cmake'):
|
| 64 |
+
raise ValueError('Installare CMake e un compilatore C++, oppure configurare llama_dir e bin_dir.')
|
| 65 |
+
print('Preparazione una tantum degli strumenti llama.cpp b6500...', flush=True)
|
| 66 |
+
with tempfile.TemporaryDirectory(prefix='.tools-', dir=root) as tmp:
|
| 67 |
+
work = Path(tmp)
|
| 68 |
+
archive = work / 'llama.tar.gz'
|
| 69 |
+
url = f'https://codeload.github.com/ggml-org/llama.cpp/tar.gz/{COMMIT}'
|
| 70 |
+
with urllib.request.urlopen(url, timeout=120) as response, archive.open('wb') as output:
|
| 71 |
+
shutil.copyfileobj(response, output)
|
| 72 |
+
if sha256(archive) != ARCHIVE_SHA256:
|
| 73 |
+
raise ValueError('SHA-256 inatteso per i sorgenti llama.cpp.')
|
| 74 |
+
with tarfile.open(archive) as tar:
|
| 75 |
+
tar.extractall(work, filter='data')
|
| 76 |
+
src, build = work / ('llama.cpp-' + COMMIT), work / 'build'
|
| 77 |
+
flags = ['-DCMAKE_BUILD_TYPE=Release', '-DGGML_NATIVE=OFF', '-DGGML_CUDA=OFF', '-DLLAMA_CURL=OFF',
|
| 78 |
+
'-DLLAMA_BUILD_TESTS=OFF', '-DLLAMA_BUILD_EXAMPLES=OFF', '-DLLAMA_BUILD_SERVER=OFF',
|
| 79 |
+
'-DBUILD_SHARED_LIBS=OFF']
|
| 80 |
+
if os.name == 'nt' and platform.machine().upper() in ('ARM64', 'AARCH64'):
|
| 81 |
+
flags += ['-G', 'Visual Studio 17 2022', '-A', 'ARM64']
|
| 82 |
+
subprocess.run(['cmake', '-S', str(src), '-B', str(build), *flags], check=True)
|
| 83 |
+
subprocess.run(['cmake', '--build', str(build), '--config', 'Release', '--parallel', '4',
|
| 84 |
+
'--target', *TARGETS], check=True)
|
| 85 |
+
compiled = build / 'bin'
|
| 86 |
+
if (compiled / 'Release').is_dir():
|
| 87 |
+
compiled = compiled / 'Release'
|
| 88 |
+
shutil.copytree(compiled, src / 'bin', dirs_exist_ok=True)
|
| 89 |
+
installed(src)
|
| 90 |
+
try:
|
| 91 |
+
src.rename(target)
|
| 92 |
+
except OSError:
|
| 93 |
+
if not target.is_dir(): # Two first runs may finish installing simultaneously.
|
| 94 |
+
raise
|
| 95 |
+
return installed(target, revision=COMMIT)
|
requests/4af9bd4213d54162b4aec991fb2a55a7/moediet.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MoEDiet MVP: build an OLMoE expert mask and a standard compact HF checkpoint."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import gc
|
| 5 |
+
import hashlib
|
| 6 |
+
import json
|
| 7 |
+
import math
|
| 8 |
+
import os
|
| 9 |
+
import shutil
|
| 10 |
+
import subprocess
|
| 11 |
+
import sys
|
| 12 |
+
import tempfile
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import transformers
|
| 17 |
+
from transformers import AutoConfig, AutoTokenizer, OlmoeForCausalLM
|
| 18 |
+
|
| 19 |
+
from fidelity import compare, make_traces, passes, replay
|
| 20 |
+
from probes import clean_tags, make_probes
|
| 21 |
+
from routing import blocks, compact_in_place, continuation, encode, masked, profile, select_experts, validate_rows
|
| 22 |
+
|
| 23 |
+
DEFAULT_MODEL = 'allenai/OLMoE-1B-7B-0125-Instruct'
|
| 24 |
+
FORMAT = 'moediet.olmoe.mask.v1'
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def write_json(path, data):
|
| 28 |
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False) + '\n', encoding='utf-8')
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def local_signature(path):
|
| 32 |
+
# Check local model identity even when a training run overwrites the same directory.
|
| 33 |
+
names = ('config.json', 'generation_config.json', 'model.safetensors.index.json', 'tokenizer.json',
|
| 34 |
+
'tokenizer_config.json', 'special_tokens_map.json', 'added_tokens.json', 'vocab.json',
|
| 35 |
+
'vocab.txt', 'merges.txt', 'chat_template.jinja')
|
| 36 |
+
files = sorted({*path.glob('*.safetensors'), *path.glob('*.model'),
|
| 37 |
+
*(path / name for name in names if (path / name).is_file())})
|
| 38 |
+
hashes = hashlib.sha256()
|
| 39 |
+
for file in files:
|
| 40 |
+
digest = hashlib.sha256()
|
| 41 |
+
with file.open('rb') as stream:
|
| 42 |
+
while chunk := stream.read(4 << 20):
|
| 43 |
+
digest.update(chunk)
|
| 44 |
+
hashes.update(file.name.encode('utf-8') + b'\x00' + digest.digest())
|
| 45 |
+
return hashes.hexdigest()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def resolve_source(name, revision, fingerprint):
|
| 49 |
+
if transformers.__version__ != '4.55.4':
|
| 50 |
+
raise ValueError('Questo adattatore richiede transformers==4.55.4 (layout del router verificato).')
|
| 51 |
+
name = os.path.expandvars(str(name))
|
| 52 |
+
path = Path(name).expanduser()
|
| 53 |
+
local = path.is_dir()
|
| 54 |
+
if local and revision:
|
| 55 |
+
raise ValueError('--revision si applica solo a un modello sullo Hub.')
|
| 56 |
+
name = str(path.resolve()) if local else name
|
| 57 |
+
cfg = AutoConfig.from_pretrained(name, revision=revision, trust_remote_code=False)
|
| 58 |
+
if cfg.model_type != 'olmoe' or getattr(cfg, 'quantization_config', None):
|
| 59 |
+
raise ValueError('Il MVP richiede pesi OLMoE non quantizzati in formato HF/safetensors.')
|
| 60 |
+
source = {'kind': 'local' if local else 'hub', 'id': name}
|
| 61 |
+
if local and fingerprint:
|
| 62 |
+
source['sha256'] = local_signature(path)
|
| 63 |
+
if not local:
|
| 64 |
+
source['revision'] = getattr(cfg, '_commit_hash', None)
|
| 65 |
+
if not source['revision']:
|
| 66 |
+
raise ValueError('Impossibile determinare il commit del checkpoint sullo Hub.')
|
| 67 |
+
return cfg, source
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def load_model(source, device, dtype):
|
| 71 |
+
tp = 'auto' if dtype == 'auto' else getattr(torch, dtype)
|
| 72 |
+
model, info = OlmoeForCausalLM.from_pretrained(
|
| 73 |
+
source['id'], revision=source.get('revision'), torch_dtype=tp, use_safetensors=True,
|
| 74 |
+
attn_implementation='eager', output_loading_info=True,
|
| 75 |
+
)
|
| 76 |
+
if any(info.get(key) for key in ('missing_keys', 'unexpected_keys', 'mismatched_keys', 'error_msgs')):
|
| 77 |
+
raise ValueError(f'Pesi incompatibili con il modello: {info}')
|
| 78 |
+
model.to(device).eval()
|
| 79 |
+
blocks(model)
|
| 80 |
+
return model
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def load_tokenizer(source):
|
| 84 |
+
return AutoTokenizer.from_pretrained(source['id'], revision=source.get('revision'), trust_remote_code=False)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def budgets_for(total, top_k, requested):
|
| 88 |
+
if not 1 <= top_k <= total:
|
| 89 |
+
raise ValueError('Numero di esperti o top-k del modello non valido.')
|
| 90 |
+
if requested is None:
|
| 91 |
+
requested = []
|
| 92 |
+
value = top_k
|
| 93 |
+
while value < total:
|
| 94 |
+
requested.append(value)
|
| 95 |
+
value *= 2
|
| 96 |
+
if any(not top_k <= x <= total for x in requested):
|
| 97 |
+
raise ValueError(f'Ogni budget deve essere fra {top_k} (routing nativo) e {total}.')
|
| 98 |
+
return sorted(set(requested) | {total})
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def topology(cfg):
|
| 102 |
+
return {'num_layers': cfg.num_hidden_layers, 'experts_per_layer': cfg.num_experts,
|
| 103 |
+
'experts_per_token': cfg.num_experts_per_tok, 'norm_topk_prob': cfg.norm_topk_prob}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def size_of(model):
|
| 107 |
+
return {'parameters': sum(p.numel() for p in model.parameters()),
|
| 108 |
+
'parameter_bytes': sum(p.numel() * p.element_size() for p in model.parameters())}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def verify_manifest(meta, model, source):
|
| 112 |
+
if meta.get('format') != FORMAT or meta.get('source') != source or meta.get('routing') != topology(model.config):
|
| 113 |
+
raise ValueError('La maschera non corrisponde alla sorgente, alla revisione o al routing del modello.')
|
| 114 |
+
rows = meta.get('expert_mask')
|
| 115 |
+
validate_rows(model, rows)
|
| 116 |
+
if meta.get('retained_per_layer') != len(rows[0]):
|
| 117 |
+
raise ValueError('Il budget dichiarato non corrisponde agli ID nella maschera.')
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@torch.inference_mode()
|
| 121 |
+
def build(args, work, device):
|
| 122 |
+
if not (math.isfinite(args.penalty) and args.penalty >= 0 and math.isfinite(args.max_kl) and args.max_kl >= 0):
|
| 123 |
+
raise ValueError('Lambda e soglia KL devono essere finite e non negative.')
|
| 124 |
+
if args.eval_tokens < 1:
|
| 125 |
+
raise ValueError('--eval-tokens deve essere positivo.')
|
| 126 |
+
tags = clean_tags(args.tags)
|
| 127 |
+
corpus = make_probes(tags, args.corpus)
|
| 128 |
+
cfg, source = resolve_source(args.model, args.revision, fingerprint=True)
|
| 129 |
+
total = cfg.num_experts
|
| 130 |
+
budgets = budgets_for(total, cfg.num_experts_per_tok, args.budgets)
|
| 131 |
+
tok = load_tokenizer(source)
|
| 132 |
+
model = load_model(source, device, args.dtype or 'auto')
|
| 133 |
+
native = topology(model.config)
|
| 134 |
+
dtype = str(model.get_input_embeddings().weight.dtype).split('.')[-1]
|
| 135 |
+
print('Profilazione dei router...', flush=True)
|
| 136 |
+
score, tokens = profile(model, tok, corpus['profile'], tags, args.penalty)
|
| 137 |
+
full_size = size_of(model)
|
| 138 |
+
report = {'source': source, 'routing': native, 'dtype': dtype, 'device': str(device),
|
| 139 |
+
'torch': str(torch.__version__), 'transformers': transformers.__version__, 'probes': corpus,
|
| 140 |
+
'profile_tokens_per_layer_by_group': tokens, 'score_by_layer': score.tolist(),
|
| 141 |
+
'lambda': args.penalty, 'budgets': budgets, 'max_kl_nats_per_token': args.max_kl,
|
| 142 |
+
'max_continuation_tokens': args.eval_tokens, 'selection': [], 'audit': None,
|
| 143 |
+
'full_model': full_size, 'checkpoint': None}
|
| 144 |
+
keep = total
|
| 145 |
+
status = 'unchanged_no_candidate'
|
| 146 |
+
if budgets[0] < total:
|
| 147 |
+
selection = make_traces(model, tok, corpus['select'], args.eval_tokens)
|
| 148 |
+
for budget in budgets:
|
| 149 |
+
if budget == total:
|
| 150 |
+
break # The identity fallback has KL = 0 by construction.
|
| 151 |
+
with masked(model, select_experts(score, budget)):
|
| 152 |
+
metrics = compare(selection, replay(model, selection))
|
| 153 |
+
passed = passes(metrics, args.max_kl)
|
| 154 |
+
report['selection'].append({'keep': budget, 'passed': passed, 'groups': metrics})
|
| 155 |
+
print(f'Budget {budget}/{total}: {"accettato" if passed else "respinto"}', flush=True)
|
| 156 |
+
if passed:
|
| 157 |
+
keep = budget
|
| 158 |
+
break
|
| 159 |
+
del selection
|
| 160 |
+
report['selected_before_audit'] = keep
|
| 161 |
+
rows = select_experts(score, keep)
|
| 162 |
+
if keep < total:
|
| 163 |
+
audit = make_traces(model, tok, corpus['audit'], args.eval_tokens)
|
| 164 |
+
with masked(model, rows):
|
| 165 |
+
masked_logp = replay(model, audit)
|
| 166 |
+
metrics = compare(audit, masked_logp)
|
| 167 |
+
passed = passes(metrics, args.max_kl)
|
| 168 |
+
report['audit'] = {'keep': keep, 'passed': passed, 'groups': metrics}
|
| 169 |
+
if passed:
|
| 170 |
+
status = 'mask_only' if args.mask_only else 'pruned'
|
| 171 |
+
else:
|
| 172 |
+
status, keep = 'unchanged_audit_failed', total
|
| 173 |
+
rows = select_experts(score, total)
|
| 174 |
+
manifest = {'format': FORMAT, 'source': source, 'routing': native, 'profile_dtype': dtype,
|
| 175 |
+
'target_tags': tags, 'retained_per_layer': keep, 'expert_mask': rows}
|
| 176 |
+
verify_manifest(manifest, model, source)
|
| 177 |
+
if status == 'pruned':
|
| 178 |
+
print('Esportazione e ricaricamento del checkpoint...', flush=True)
|
| 179 |
+
path = work / 'checkpoint'
|
| 180 |
+
compact_in_place(model, rows)
|
| 181 |
+
model.save_pretrained(path, safe_serialization=True, max_shard_size='2GB')
|
| 182 |
+
tok.save_pretrained(path)
|
| 183 |
+
del model
|
| 184 |
+
gc.collect()
|
| 185 |
+
compact = load_model({'id': str(path)}, device, dtype)
|
| 186 |
+
compact_logp = replay(compact, audit)
|
| 187 |
+
actual = compare(audit, compact_logp)
|
| 188 |
+
passed = passes(actual, args.max_kl)
|
| 189 |
+
report['checkpoint_audit'] = {'passed': passed, 'vs_original': actual,
|
| 190 |
+
'vs_mask': compare(audit, compact_logp, masked_logp)}
|
| 191 |
+
if passed:
|
| 192 |
+
report['checkpoint'] = {**size_of(compact), 'safetensors_bytes': sum(
|
| 193 |
+
file.stat().st_size for file in path.glob('*.safetensors'))}
|
| 194 |
+
else:
|
| 195 |
+
status = 'mask_only_checkpoint_audit_failed'
|
| 196 |
+
del compact
|
| 197 |
+
gc.collect()
|
| 198 |
+
if not passed:
|
| 199 |
+
shutil.rmtree(path) # Only this run's unpublished temporary export.
|
| 200 |
+
report.update(status=status, retained_per_layer=keep, expert_fraction_removed=1 - keep / total)
|
| 201 |
+
write_json(work / 'mask.json', manifest)
|
| 202 |
+
write_json(work / 'report.json', report)
|
| 203 |
+
return report
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@torch.inference_mode()
|
| 207 |
+
def generate(args, device):
|
| 208 |
+
if args.max_new_tokens < 1 or not args.prompt.strip():
|
| 209 |
+
raise ValueError('Servono un prompt non vuoto e un numero di token positivo.')
|
| 210 |
+
meta = None
|
| 211 |
+
if args.mask:
|
| 212 |
+
if args.revision:
|
| 213 |
+
raise ValueError('La revisione è già fissata nella maschera.')
|
| 214 |
+
meta = json.loads(Path(args.mask).read_text(encoding='utf-8'))
|
| 215 |
+
if meta.get('format') != FORMAT:
|
| 216 |
+
raise ValueError('Formato maschera non riconosciuto.')
|
| 217 |
+
original = meta['source']
|
| 218 |
+
if original['kind'] == 'local' and not Path(original['id']).is_dir():
|
| 219 |
+
raise ValueError('La directory locale originale registrata nella maschera non è disponibile.')
|
| 220 |
+
_, source = resolve_source(original['id'], original.get('revision'), fingerprint=True)
|
| 221 |
+
if source != original:
|
| 222 |
+
raise ValueError('Il checkpoint originale è cambiato dalla profilazione.')
|
| 223 |
+
else:
|
| 224 |
+
_, source = resolve_source(args.model, args.revision, fingerprint=False)
|
| 225 |
+
dtype = args.dtype or (meta['profile_dtype'] if meta else 'auto')
|
| 226 |
+
model, tok = load_model(source, device, dtype), load_tokenizer(source)
|
| 227 |
+
ids = encode(model, tok, args.prompt, reserve=args.max_new_tokens)
|
| 228 |
+
if meta:
|
| 229 |
+
verify_manifest(meta, model, source)
|
| 230 |
+
with masked(model, meta['expert_mask']):
|
| 231 |
+
seq = continuation(model, tok, ids, args.max_new_tokens)
|
| 232 |
+
else:
|
| 233 |
+
seq = continuation(model, tok, ids, args.max_new_tokens)
|
| 234 |
+
return tok.decode(seq[0, ids.shape[1]:].tolist(), skip_special_tokens=True)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def main(argv=None):
|
| 238 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 239 |
+
subs = parser.add_subparsers(dest='command', required=True)
|
| 240 |
+
builder = subs.add_parser('build', help='Profilazione, selezione, audit ed export')
|
| 241 |
+
builder.add_argument('--model', default=DEFAULT_MODEL, help='ID Hub o directory OLMoE locale')
|
| 242 |
+
builder.add_argument('--tags', nargs='+', required=True)
|
| 243 |
+
builder.add_argument('--out', type=Path, required=True, help='Nuova directory dei risultati')
|
| 244 |
+
builder.add_argument('--budgets', nargs='+', type=int, help='Esperti conservati per strato, fallback intero automatico')
|
| 245 |
+
builder.add_argument('--lambda', dest='penalty', type=float, default=0.5)
|
| 246 |
+
builder.add_argument('--max-kl', type=float, default=0.10, help='Soglia per ogni gruppo, in nat/token')
|
| 247 |
+
builder.add_argument('--eval-tokens', type=int, default=8)
|
| 248 |
+
builder.add_argument('--corpus', type=Path, help='JSON di sonde personalizzate')
|
| 249 |
+
builder.add_argument('--mask-only', action='store_true')
|
| 250 |
+
builder.add_argument('--gguf', choices=['Q4_K_M', 'Q4_0'], help='Esporta anche il checkpoint in GGUF Q4')
|
| 251 |
+
builder.add_argument('--llama-dir', type=Path, help='Checkout llama.cpp per --gguf')
|
| 252 |
+
builder.add_argument('--bin-dir', type=Path, help='Directory degli eseguibili della stessa build')
|
| 253 |
+
gen = subs.add_parser('generate', help='Inferenza greedy con maschera o checkpoint HF')
|
| 254 |
+
sources = gen.add_mutually_exclusive_group(required=True)
|
| 255 |
+
sources.add_argument('--model', help='Checkpoint HF completo o compatto')
|
| 256 |
+
sources.add_argument('--mask', type=Path, help='Maschera JSON con riferimento al checkpoint originale')
|
| 257 |
+
gen.add_argument('--prompt', required=True)
|
| 258 |
+
gen.add_argument('--max-new-tokens', type=int, default=48)
|
| 259 |
+
for sub in (builder, gen):
|
| 260 |
+
sub.add_argument('--device', choices=['auto', 'cpu', 'cuda'], default='auto')
|
| 261 |
+
sub.add_argument('--dtype', choices=['auto', 'float32', 'float16', 'bfloat16'])
|
| 262 |
+
sub.add_argument('--revision', help='Commit o ref del modello sullo Hub')
|
| 263 |
+
args = parser.parse_args(argv)
|
| 264 |
+
device = ('cuda' if torch.cuda.is_available() else 'cpu') if args.device == 'auto' else args.device
|
| 265 |
+
if args.command == 'generate':
|
| 266 |
+
if args.mask:
|
| 267 |
+
args.mask = Path(os.path.expandvars(str(args.mask))).expanduser()
|
| 268 |
+
if args.model:
|
| 269 |
+
args.model = os.path.expandvars(str(args.model))
|
| 270 |
+
print(generate(args, device))
|
| 271 |
+
return
|
| 272 |
+
if args.corpus:
|
| 273 |
+
args.corpus = Path(os.path.expandvars(str(args.corpus))).expanduser()
|
| 274 |
+
if args.llama_dir:
|
| 275 |
+
args.llama_dir = Path(os.path.expandvars(str(args.llama_dir))).expanduser()
|
| 276 |
+
if args.bin_dir:
|
| 277 |
+
args.bin_dir = Path(os.path.expandvars(str(args.bin_dir))).expanduser()
|
| 278 |
+
if args.mask_only and args.gguf:
|
| 279 |
+
raise ValueError('--gguf richiede il checkpoint fisico: non si combina con --mask-only.')
|
| 280 |
+
if (args.llama_dir or args.bin_dir) and not args.gguf:
|
| 281 |
+
raise ValueError('--llama-dir e --bin-dir richiedono --gguf.')
|
| 282 |
+
if args.bin_dir and not args.llama_dir:
|
| 283 |
+
raise ValueError('--bin-dir richiede --llama-dir.')
|
| 284 |
+
out = Path(os.path.expandvars(str(args.out))).expanduser().resolve()
|
| 285 |
+
if out.exists():
|
| 286 |
+
raise ValueError(f'La destinazione esiste già: {out}. Scegliere una nuova directory.')
|
| 287 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 288 |
+
with tempfile.TemporaryDirectory(prefix='.moediet-', dir=out.parent) as temp:
|
| 289 |
+
stage = Path(temp) / 'result'
|
| 290 |
+
stage.mkdir()
|
| 291 |
+
report = build(args, stage, device)
|
| 292 |
+
stage.rename(out)
|
| 293 |
+
print(f"{report['status']} | {report['retained_per_layer']}/{report['routing']['experts_per_layer']} esperti/strato | {out}")
|
| 294 |
+
if args.gguf:
|
| 295 |
+
from gguf_export import export_run
|
| 296 |
+
export_run(out, args.gguf, args.llama_dir, args.bin_dir)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
if __name__ == '__main__':
|
| 300 |
+
try:
|
| 301 |
+
main()
|
| 302 |
+
except (ValueError, OSError, ImportError, subprocess.SubprocessError) as error:
|
| 303 |
+
sys.exit(f'Errore: {error}')
|
requests/4af9bd4213d54162b4aec991fb2a55a7/probes.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small, inspectable probe bank. No generator model or network calls."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
CONTROL = "__control__"
|
| 7 |
+
SPLITS = ("profile", "select", "audit")
|
| 8 |
+
# Two distinct situations per split. The same stems are used for target and control.
|
| 9 |
+
STEMS = (
|
| 10 |
+
"Explain the mechanism involved.", "Give a concrete solution and justify it.",
|
| 11 |
+
"What would you check first, and why?", "Work through a small example.",
|
| 12 |
+
"Explain what happens next and why.", "Show how you would verify your answer.",
|
| 13 |
+
)
|
| 14 |
+
CASES = {
|
| 15 |
+
"python": ("A mutable default list accumulates items between calls.", "Read a large CSV lazily using a generator.",
|
| 16 |
+
"A closure in a loop returns the last index every time.", "Group log entries using collections.defaultdict.",
|
| 17 |
+
"Two names refer to the same nested list after a shallow copy.", "Test an asynchronous context manager."),
|
| 18 |
+
"c++": ("A std::vector reallocation invalidates an iterator.", "Transfer sole ownership using std::unique_ptr.",
|
| 19 |
+
"A lambda captures a local variable by reference and escapes.", "Implement a move constructor for a buffer.",
|
| 20 |
+
"A std::string_view outlives its backing string.", "Test destruction through a polymorphic base pointer."),
|
| 21 |
+
"devops": ("A rolling deployment serves traffic before readiness checks pass.", "Roll back a failed deployment without losing logs.",
|
| 22 |
+
"A deployment succeeds in staging but fails its production health check.", "Choose a threshold for a canary rollback.",
|
| 23 |
+
"A Kubernetes pod is repeatedly OOMKilled.", "Restore a backup in isolation and verify the restored data."),
|
| 24 |
+
"docker": ("A source change invalidates every Docker build layer.", "Produce a small runtime image with a multi-stage Dockerfile.",
|
| 25 |
+
"A file copied into an image disappears under a bind mount.", "Configure a container health check.",
|
| 26 |
+
"A PID 1 shell does not forward SIGTERM to its child.", "Verify that a named volume survives container replacement."),
|
| 27 |
+
"postgresql": ("An index is ignored on a low-selectivity column.", "Inspect a slow query with EXPLAIN ANALYZE.",
|
| 28 |
+
"Two transactions acquire row locks in opposite order.", "Build a partial index for unprocessed jobs.",
|
| 29 |
+
"An old transaction prevents VACUUM from reclaiming tuples.", "Verify a retry after a serialization failure."),
|
| 30 |
+
"sql": ("A LEFT JOIN loses unmatched rows after a WHERE filter.", "Rank sales totals within each region.",
|
| 31 |
+
"A join duplicates each invoice total.", "Find customers with no orders using NOT EXISTS.",
|
| 32 |
+
"COUNT(column) skips NULL values in a grouped query.", "Test a running total when two rows share a date."),
|
| 33 |
+
"gardening": ("Leaves yellow after overwatering.", "Prepare soil for planting carrots.", "Seedlings bend toward a window.",
|
| 34 |
+
"Choose a watering schedule for a clay pot.", "A late frost damages new shoots.", "Check whether stored seeds will germinate."),
|
| 35 |
+
"navigation": ("A compass bearing differs from true north.", "Estimate walking time using a contour map.", "Two map landmarks seem misaligned.",
|
| 36 |
+
"Convert a map scale into walking distance.", "Wind pushes a boat away from its intended bearing.", "Check a position using two bearings."),
|
| 37 |
+
"music": ("Two strings produce beats when played together.", "Transpose a melody to a new key.", "A chord sounds unstable before resolving.",
|
| 38 |
+
"Count the pulses in six-eight time.", "Shortening a vibrating string raises its pitch.", "Check whether a recording is in tune."),
|
| 39 |
+
}
|
| 40 |
+
ALIASES = {"cpp": "c++", "postgres": "postgresql"}
|
| 41 |
+
GENERAL = ("gardening", "navigation", "music")
|
| 42 |
+
NEUTRAL = ("A calendar spans a leap day.", "Divide a grocery bill between three friends.",
|
| 43 |
+
"Two clocks disagree after a battery change.", "Find the median of five distances.",
|
| 44 |
+
"A queue is served in arrival order.", "Check a temperature conversion by reversing it.")
|
| 45 |
+
GENERIC = ("Introduce a basic concept with a worked example.", "Correct a common beginner mistake.",
|
| 46 |
+
"Diagnose an unexpected outcome on a typical task.", "Compare two methods for a familiar problem.",
|
| 47 |
+
"Predict an edge case in a realistic scenario.", "Design a reproducible check for a subtle error.")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def clean_tags(tags):
|
| 51 |
+
names = [t.strip().casefold() for t in tags]
|
| 52 |
+
names = list(dict.fromkeys(ALIASES.get(t, t) for t in names))
|
| 53 |
+
if not names or any(t in ('', CONTROL) for t in names):
|
| 54 |
+
raise ValueError(f"Serve almeno un tag non vuoto. {CONTROL} è riservato.")
|
| 55 |
+
return names
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def prompts_for(topic, split_index):
|
| 59 |
+
cases = CASES.get(topic, GENERIC)
|
| 60 |
+
return [f"Topic: {topic}. {cases[i]} {STEMS[i]}" for i in range(2 * split_index, 2 * split_index + 2)]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def make_probes(tags, custom=None):
|
| 64 |
+
if custom:
|
| 65 |
+
corpus = json.loads(Path(custom).read_text(encoding="utf-8"))
|
| 66 |
+
else:
|
| 67 |
+
controls = [topic for topic in GENERAL if topic not in tags]
|
| 68 |
+
corpus = {}
|
| 69 |
+
for i, split in enumerate(SPLITS):
|
| 70 |
+
groups = {tag: prompts_for(tag, i) for tag in tags}
|
| 71 |
+
groups[CONTROL] = [prompt for topic in controls for prompt in prompts_for(topic, i)] or [
|
| 72 |
+
f"{NEUTRAL[j]} {STEMS[j]}" for j in range(2 * i, 2 * i + 2)]
|
| 73 |
+
corpus[split] = groups
|
| 74 |
+
if not isinstance(corpus, dict) or set(corpus) != set(SPLITS):
|
| 75 |
+
raise ValueError(f"Il corpus richiede esattamente questi split: {SPLITS}")
|
| 76 |
+
seen = set()
|
| 77 |
+
for split, groups in corpus.items():
|
| 78 |
+
if not isinstance(groups, dict) or set(groups) != {*tags, CONTROL}:
|
| 79 |
+
raise ValueError(f"Gruppi errati in {split}. Servono tutti i tag e {CONTROL}.")
|
| 80 |
+
for group, prompts in groups.items():
|
| 81 |
+
if not isinstance(prompts, list) or not prompts:
|
| 82 |
+
raise ValueError(f"Nessuna sonda in {split}/{group}. Fornire un corpus con esempi distinti.")
|
| 83 |
+
for prompt in prompts:
|
| 84 |
+
if not isinstance(prompt, str) or not prompt.strip():
|
| 85 |
+
raise ValueError(f"Sonda vuota o non testuale in {split}/{group}.")
|
| 86 |
+
key = ' '.join(prompt.casefold().split())
|
| 87 |
+
if key in seen:
|
| 88 |
+
raise ValueError(f"Sonda duplicata nel corpus: {prompt!r}")
|
| 89 |
+
seen.add(key)
|
| 90 |
+
return corpus
|
requests/4af9bd4213d54162b4aec991fb2a55a7/request.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"model": "allenai/OLMoE-1B-7B-0125-Instruct", "tags": ["python", "c++", "devops"], "quantization": "Q4_K_M", "build": {"device": "cuda", "dtype": "bfloat16", "revision": "b89a7c4bc24fb9e55ce2543c9458ce0ca5c4650e"}}
|
requests/4af9bd4213d54162b4aec991fb2a55a7/requirements-q4.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-r requirements.txt
|
| 2 |
+
# Imports used by the pinned upstream GGUF converter.
|
| 3 |
+
mistral-common==1.8.5
|
| 4 |
+
sentencepiece>=0.2,<0.3
|
| 5 |
+
protobuf>=4.25,<8
|
| 6 |
+
PyYAML>=6,<7
|
requests/4af9bd4213d54162b4aec991fb2a55a7/requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install a PyTorch build for your platform before this file if necessary.
|
| 2 |
+
torch>=2.6,<3
|
| 3 |
+
transformers==4.55.4
|
| 4 |
+
safetensors>=0.4.3,<1
|
requests/4af9bd4213d54162b4aec991fb2a55a7/routing.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OLMoE adapter for Transformers 4.55.4, with native top-k preserved."""
|
| 2 |
+
|
| 3 |
+
from contextlib import ExitStack, contextmanager
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def blocks(model):
|
| 10 |
+
cfg = model.config
|
| 11 |
+
if cfg.model_type != 'olmoe' or not 1 <= cfg.num_experts_per_tok <= cfg.num_experts:
|
| 12 |
+
raise ValueError('Richiesto un checkpoint OLMoE con top-k valido.')
|
| 13 |
+
layers = [layer.mlp for layer in model.model.layers]
|
| 14 |
+
if not layers or len(layers) != cfg.num_hidden_layers:
|
| 15 |
+
raise ValueError('Numero di strati OLMoE incoerente.')
|
| 16 |
+
for block in layers:
|
| 17 |
+
if not (isinstance(block.gate, nn.Linear) and block.gate.bias is None
|
| 18 |
+
and isinstance(block.experts, nn.ModuleList)
|
| 19 |
+
and block.num_experts == block.gate.out_features == len(block.experts) == cfg.num_experts
|
| 20 |
+
and block.top_k == cfg.num_experts_per_tok):
|
| 21 |
+
raise ValueError('Layout del router non supportato. Usare la versione Transformers in requirements.txt.')
|
| 22 |
+
return layers
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def encode(model, tokenizer, prompt, reserve=0):
|
| 26 |
+
if tokenizer.chat_template:
|
| 27 |
+
ids = tokenizer.apply_chat_template([{'role': 'user', 'content': prompt}], tokenize=True,
|
| 28 |
+
add_generation_prompt=True, return_tensors='pt')
|
| 29 |
+
else:
|
| 30 |
+
ids = tokenizer(prompt, return_tensors='pt')['input_ids']
|
| 31 |
+
if not ids.shape[1] or ids.shape[1] + reserve > model.config.max_position_embeddings:
|
| 32 |
+
raise ValueError(f'Sonda vuota o oltre il contesto: {ids.shape[1]} token + {reserve} riservati.')
|
| 33 |
+
return ids.to(model.get_input_embeddings().weight.device)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def continuation(model, tokenizer, ids, count):
|
| 37 |
+
pad_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
|
| 38 |
+
return model.generate(input_ids=ids, attention_mask=torch.ones_like(ids), max_new_tokens=count,
|
| 39 |
+
do_sample=False, num_beams=1, use_cache=True, pad_token_id=pad_id,
|
| 40 |
+
return_dict_in_generate=False, output_router_logits=False)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@torch.inference_mode()
|
| 44 |
+
def profile(model, tokenizer, groups, tags, penalty):
|
| 45 |
+
from probes import CONTROL
|
| 46 |
+
|
| 47 |
+
layers = blocks(model)
|
| 48 |
+
experts = model.config.num_experts
|
| 49 |
+
means, counts = {}, {}
|
| 50 |
+
for group, prompts in groups.items():
|
| 51 |
+
sums = torch.zeros(len(layers), experts, dtype=torch.float64)
|
| 52 |
+
counts[group] = 0
|
| 53 |
+
for prompt in prompts:
|
| 54 |
+
ids = encode(model, tokenizer, prompt)
|
| 55 |
+
# Single unpadded prompt. The backbone returns logits without the LM head or auxiliary loss.
|
| 56 |
+
routers = model.model(input_ids=ids, use_cache=False, output_router_logits=True,
|
| 57 |
+
return_dict=True).router_logits
|
| 58 |
+
count = ids.numel()
|
| 59 |
+
if len(routers) != len(layers):
|
| 60 |
+
raise ValueError('Logit di uno o più router mancanti.')
|
| 61 |
+
for i, (block, logits) in enumerate(zip(layers, routers)):
|
| 62 |
+
if tuple(logits.shape) != (count, experts):
|
| 63 |
+
raise ValueError(f'Forma inattesa dei logit del router {i}: {tuple(logits.shape)}')
|
| 64 |
+
probs = logits.float().softmax(-1)
|
| 65 |
+
weights, chosen = probs.topk(block.top_k, dim=-1)
|
| 66 |
+
if block.norm_topk_prob:
|
| 67 |
+
weights = weights / weights.sum(-1, keepdim=True)
|
| 68 |
+
mass = torch.zeros_like(probs).scatter_(-1, chosen, weights)
|
| 69 |
+
sums[i] += mass.sum(dim=0, dtype=torch.float64).cpu()
|
| 70 |
+
counts[group] += count # Every layer has seen these same tokens exactly once.
|
| 71 |
+
means[group] = sums / counts[group]
|
| 72 |
+
target = torch.stack([means[tag] for tag in tags]).mean(0)
|
| 73 |
+
score = target - penalty * means[CONTROL]
|
| 74 |
+
if not torch.isfinite(score).all():
|
| 75 |
+
raise ValueError('Lo score contiene valori non finiti.')
|
| 76 |
+
return score, counts
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def select_experts(score, keep):
|
| 80 |
+
# Stable sort resolves equal scores in favour of the lower original ID.
|
| 81 |
+
ranked = torch.argsort(score, dim=1, descending=True, stable=True)
|
| 82 |
+
return ranked[:, :keep].sort(dim=1).values.tolist()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def validate_rows(model, rows):
|
| 86 |
+
layers = blocks(model)
|
| 87 |
+
cfg = model.config
|
| 88 |
+
if not isinstance(rows, list) or len(rows) != len(layers):
|
| 89 |
+
raise ValueError('La maschera deve contenere una lista per ogni strato, in ordine.')
|
| 90 |
+
for row in rows:
|
| 91 |
+
if (not isinstance(row, list) or not cfg.num_experts_per_tok <= len(row) <= cfg.num_experts
|
| 92 |
+
or any(type(i) is not int or not 0 <= i < cfg.num_experts for i in row)
|
| 93 |
+
or row != sorted(set(row)) or len(row) != len(rows[0])):
|
| 94 |
+
raise ValueError('Maschera non valida: servono ID unici ordinati e un budget uniforme >= top-k.')
|
| 95 |
+
return layers
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@contextmanager
|
| 99 |
+
def masked(model, rows):
|
| 100 |
+
layers = validate_rows(model, rows)
|
| 101 |
+
with ExitStack() as stack:
|
| 102 |
+
for block, ids in zip(layers, rows):
|
| 103 |
+
if len(ids) == block.num_experts:
|
| 104 |
+
continue
|
| 105 |
+
excluded = torch.ones(block.num_experts, device=block.gate.weight.device, dtype=torch.bool)
|
| 106 |
+
excluded[ids] = False
|
| 107 |
+
|
| 108 |
+
def mask_logits(module, args, logits, excluded=excluded):
|
| 109 |
+
return logits.masked_fill(excluded, -torch.inf)
|
| 110 |
+
|
| 111 |
+
handle = block.gate.register_forward_hook(mask_logits)
|
| 112 |
+
stack.callback(handle.remove)
|
| 113 |
+
yield
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def compact_in_place(model, rows):
|
| 117 |
+
# This consumes the original model. Only gate rows are copied, retained MLPs are reused.
|
| 118 |
+
layers = validate_rows(model, rows)
|
| 119 |
+
keep = len(rows[0])
|
| 120 |
+
for block, ids in zip(layers, rows):
|
| 121 |
+
old = block.gate
|
| 122 |
+
index = torch.tensor(ids, device=old.weight.device)
|
| 123 |
+
gate = nn.Linear(old.in_features, keep, bias=False, device=old.weight.device, dtype=old.weight.dtype)
|
| 124 |
+
gate.weight = nn.Parameter(old.weight.index_select(0, index), requires_grad=old.weight.requires_grad)
|
| 125 |
+
block.gate = gate
|
| 126 |
+
block.experts = nn.ModuleList([block.experts[i] for i in ids])
|
| 127 |
+
block.num_experts = keep
|
| 128 |
+
model.num_experts = model.config.num_experts = keep
|
| 129 |
+
model.eval()
|
requests/4af9bd4213d54162b4aec991fb2a55a7/run.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Launch the same MoEDiet pipeline locally or on Hugging Face Jobs."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import subprocess
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
from gguf_export import QTYPES, export_run
|
| 13 |
+
from probes import clean_tags, make_probes
|
| 14 |
+
|
| 15 |
+
HERE = Path(__file__).resolve().parent
|
| 16 |
+
DEFAULT_MODEL = 'allenai/OLMoE-1B-7B-0125-Instruct'
|
| 17 |
+
BUILD_FLAGS = {'budgets': '--budgets', 'lambda': '--lambda', 'max_kl': '--max-kl',
|
| 18 |
+
'eval_tokens': '--eval-tokens', 'dtype': '--dtype', 'device': '--device', 'revision': '--revision'}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def path_from(base, value):
|
| 22 |
+
expanded = os.path.expandvars(str(value))
|
| 23 |
+
path = Path(expanded).expanduser()
|
| 24 |
+
if path.is_absolute():
|
| 25 |
+
return str(path.resolve())
|
| 26 |
+
return str((base / path).resolve())
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def load_config(filename, tags=None, repo=None):
|
| 30 |
+
filename = Path(os.path.expandvars(str(filename))).expanduser().resolve()
|
| 31 |
+
raw = json.loads(filename.read_text(encoding='utf-8-sig'))
|
| 32 |
+
if not isinstance(raw, dict) or raw.get('mode') not in ('local', 'hf'):
|
| 33 |
+
raise ValueError('Il profilo JSON richiede mode uguale a local o hf.')
|
| 34 |
+
local = raw['mode'] == 'local'
|
| 35 |
+
common = {'mode', 'model', 'tags', 'quantization', 'build', 'corpus'}
|
| 36 |
+
extra = {'out', 'llama_dir', 'bin_dir'} if local else {'output_repo', 'flavor', 'timeout'}
|
| 37 |
+
if unknown := set(raw) - common - extra:
|
| 38 |
+
raise ValueError(f'Chiavi di configurazione sconosciute: {sorted(unknown)}')
|
| 39 |
+
names = raw.get('tags') if tags is None else tags
|
| 40 |
+
if not isinstance(names, list) or not all(isinstance(name, str) for name in names):
|
| 41 |
+
raise ValueError('tags deve essere una lista di stringhe.')
|
| 42 |
+
cfg = {'mode': raw['mode'], 'tags': clean_tags(names), 'model': raw.get('model', DEFAULT_MODEL),
|
| 43 |
+
'quantization': raw.get('quantization', 'Q4_K_M'), 'build': raw.get('build', {})}
|
| 44 |
+
if cfg['quantization'] not in (*QTYPES, None):
|
| 45 |
+
raise ValueError(f'quantization deve essere null oppure uno tra {QTYPES}.')
|
| 46 |
+
if not isinstance(cfg['model'], str) or not cfg['model'].strip():
|
| 47 |
+
raise ValueError('model deve contenere un ID Hub o una cartella locale.')
|
| 48 |
+
build = cfg['build']
|
| 49 |
+
if not isinstance(build, dict) or set(build) - set(BUILD_FLAGS):
|
| 50 |
+
raise ValueError(f'Parametri build consentiti: {list(BUILD_FLAGS)}')
|
| 51 |
+
for key in ('lambda', 'max_kl'):
|
| 52 |
+
value = build.get(key, 0)
|
| 53 |
+
if type(value) not in (int, float) or not math.isfinite(value) or value < 0:
|
| 54 |
+
raise ValueError(f'Valore non valido per {key}.')
|
| 55 |
+
if type(build.get('eval_tokens', 8)) is not int or build.get('eval_tokens', 8) < 1:
|
| 56 |
+
raise ValueError('eval_tokens deve essere un intero positivo.')
|
| 57 |
+
if 'budgets' in build and (not isinstance(build['budgets'], list) or not build['budgets'] or
|
| 58 |
+
any(type(n) is not int or n < 1 for n in build['budgets'])):
|
| 59 |
+
raise ValueError('budgets deve essere una lista di interi positivi.')
|
| 60 |
+
if build.get('device', 'auto') not in ('auto', 'cpu', 'cuda') or build.get('dtype', 'auto') not in (
|
| 61 |
+
'auto', 'float32', 'bfloat16', 'float16'):
|
| 62 |
+
raise ValueError('device o dtype non valido.')
|
| 63 |
+
if 'revision' in build and (not isinstance(build['revision'], str) or not build['revision'].strip()):
|
| 64 |
+
raise ValueError('revision deve essere una stringa non vuota.')
|
| 65 |
+
if raw.get('corpus'):
|
| 66 |
+
cfg['corpus'] = path_from(filename.parent, raw['corpus'])
|
| 67 |
+
make_probes(cfg['tags'], cfg['corpus'])
|
| 68 |
+
if local:
|
| 69 |
+
if repo:
|
| 70 |
+
raise ValueError('--repo si usa con mode=hf.')
|
| 71 |
+
if not isinstance(raw.get('out'), str) or not raw['out'].strip():
|
| 72 |
+
raise ValueError('Indicare una nuova cartella out.')
|
| 73 |
+
cfg['out'] = path_from(filename.parent, raw['out'])
|
| 74 |
+
local_model = Path(path_from(filename.parent, cfg['model']))
|
| 75 |
+
if local_model.is_dir():
|
| 76 |
+
cfg['model'] = str(local_model)
|
| 77 |
+
for key in ('llama_dir', 'bin_dir'):
|
| 78 |
+
if raw.get(key):
|
| 79 |
+
cfg[key] = path_from(filename.parent, raw[key])
|
| 80 |
+
if cfg.get('bin_dir') and not cfg.get('llama_dir'):
|
| 81 |
+
raise ValueError('bin_dir richiede llama_dir.')
|
| 82 |
+
else:
|
| 83 |
+
from huggingface_hub.utils import validate_repo_id
|
| 84 |
+
cfg['output_repo'] = repo or raw.get('output_repo')
|
| 85 |
+
for name in (cfg['model'], cfg['output_repo']):
|
| 86 |
+
if not isinstance(name, str) or name.count('/') != 1:
|
| 87 |
+
raise ValueError('HF richiede ID nome/modello. Specificare --repo nome/repo-privato per il risultato.')
|
| 88 |
+
validate_repo_id(name)
|
| 89 |
+
if cfg['output_repo'].casefold() == cfg['model'].casefold():
|
| 90 |
+
raise ValueError('Sorgente e repository di destinazione devono essere distinti.')
|
| 91 |
+
cfg.update(flavor=raw.get('flavor', 'l4x1'), timeout=raw.get('timeout', '90m'))
|
| 92 |
+
if not isinstance(cfg['timeout'], str) or not re.fullmatch(r'[1-9]\d*[smhd]', cfg['timeout']):
|
| 93 |
+
raise ValueError('Timeout non valido (per esempio 90m).')
|
| 94 |
+
if not isinstance(cfg['flavor'], str) or not re.fullmatch(r'[a-z0-9-]+', cfg['flavor']):
|
| 95 |
+
raise ValueError('Flavor HF non valido.')
|
| 96 |
+
return cfg
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def build_command(cfg, out):
|
| 100 |
+
cmd = [sys.executable, str(HERE / 'moediet.py'), 'build', '--model', cfg['model'],
|
| 101 |
+
'--tags', *cfg['tags'], '--out', str(out)]
|
| 102 |
+
for name, flag in BUILD_FLAGS.items():
|
| 103 |
+
if name in cfg['build']:
|
| 104 |
+
value = cfg['build'][name]
|
| 105 |
+
values = value if isinstance(value, list) else [value]
|
| 106 |
+
cmd.extend([flag, *map(str, values)])
|
| 107 |
+
if cfg.get('corpus'):
|
| 108 |
+
cmd.extend(['--corpus', cfg['corpus']])
|
| 109 |
+
return cmd
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def execute(cfg, out, publish=None):
|
| 113 |
+
subprocess.run(build_command(cfg, out), check=True)
|
| 114 |
+
if publish:
|
| 115 |
+
publish(out, '') # Keep the audited result even if the subsequent conversion fails.
|
| 116 |
+
if cfg['quantization']:
|
| 117 |
+
folder = export_run(out, cfg['quantization'], cfg.get('llama_dir'), cfg.get('bin_dir'))
|
| 118 |
+
if folder and publish:
|
| 119 |
+
publish(folder, folder.relative_to(out).as_posix())
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def main(argv=None):
|
| 123 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 124 |
+
parser.add_argument('--config', type=lambda p: Path(os.path.expandvars(p)).expanduser(), required=True)
|
| 125 |
+
parser.add_argument('--tags', nargs='+', help='Sostituisce i tag del profilo')
|
| 126 |
+
parser.add_argument('--repo', help='Repository HF privato di destinazione, nome/repo')
|
| 127 |
+
parser.add_argument('--dry-run', action='store_true', help='Convalida il piano senza download, upload o Job')
|
| 128 |
+
args = parser.parse_args(argv)
|
| 129 |
+
cfg = load_config(args.config, args.tags, args.repo)
|
| 130 |
+
if args.dry_run:
|
| 131 |
+
print(json.dumps(cfg, ensure_ascii=False, indent=2))
|
| 132 |
+
return
|
| 133 |
+
if cfg['mode'] == 'local':
|
| 134 |
+
execute(cfg, Path(cfg['out']))
|
| 135 |
+
else:
|
| 136 |
+
from hf_jobs import submit
|
| 137 |
+
submit(cfg)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
if __name__ == '__main__':
|
| 141 |
+
try:
|
| 142 |
+
main()
|
| 143 |
+
except (ValueError, OSError, ImportError, subprocess.SubprocessError) as error:
|
| 144 |
+
sys.exit(f'Errore: {error}')
|