ioxdue commited on
Commit
da84a67
·
verified ·
1 Parent(s): f9801d4

MoEDiet: codice e parametri del Job

Browse files
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}')