Publish Ling-3.0-tiny RKNN engine and model (part 2)
Browse filesIncludes the final RK3588 model, verified portable runtime, canonical C++ source, conversion tooling, evaluation docs, and third-party notices.
tools/validate_selective_release_board.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Isolated portable release acceptance; restore the existing 64K service on exit."""
|
| 3 |
+
import argparse
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import signal
|
| 9 |
+
import socket
|
| 10 |
+
import statistics
|
| 11 |
+
import subprocess
|
| 12 |
+
import tempfile
|
| 13 |
+
import time
|
| 14 |
+
import urllib.request
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
p=argparse.ArgumentParser(description=__doc__)
|
| 19 |
+
p.add_argument('--root',type=Path,required=True)
|
| 20 |
+
a=p.parse_args()
|
| 21 |
+
root=a.root.resolve()
|
| 22 |
+
package=root/'package/mindnano-ling3-rk3588-selective-20260911'
|
| 23 |
+
output=root/'validation'
|
| 24 |
+
output.mkdir(exist_ok=False)
|
| 25 |
+
deployment=root/'deployment/mindnano-ling3-rk3588'
|
| 26 |
+
reference=root/'calibrated-selective-20260911/kda_out_mla_in_mla_out_shared_dense_head'
|
| 27 |
+
suite=root/'mixed-precision-reference/suite.json'
|
| 28 |
+
env={'PATH':'/usr/bin:/bin','LANG':'C.UTF-8','no_proxy':'*'}
|
| 29 |
+
client=urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
| 30 |
+
result={}
|
| 31 |
+
def request(port,path,body=None):
|
| 32 |
+
r=urllib.request.Request(f'http://127.0.0.1:{port}'+path,
|
| 33 |
+
data=None if body is None else json.dumps(body).encode(),headers={'Content-Type':'application/json'})
|
| 34 |
+
with client.open(r,timeout=600) as f:return json.load(f)
|
| 35 |
+
def ready(port,child=None):
|
| 36 |
+
deadline=time.monotonic()+600
|
| 37 |
+
while time.monotonic()<deadline:
|
| 38 |
+
if child is not None and child.poll() is not None:raise RuntimeError('service exited: '+str(child.returncode))
|
| 39 |
+
try:
|
| 40 |
+
h=request(port,'/health')
|
| 41 |
+
if h['status']=='ok':return h
|
| 42 |
+
except (OSError,ValueError):pass
|
| 43 |
+
time.sleep(1)
|
| 44 |
+
raise TimeoutError('service initialization')
|
| 45 |
+
def stop(child):
|
| 46 |
+
if child.poll() is None:
|
| 47 |
+
child.terminate()
|
| 48 |
+
try:child.wait(timeout=30)
|
| 49 |
+
except subprocess.TimeoutExpired:
|
| 50 |
+
os.killpg(child.pid,signal.SIGKILL);child.wait()
|
| 51 |
+
raise RuntimeError('test process required SIGKILL')
|
| 52 |
+
if child.returncode:raise RuntimeError('test process exit: '+str(child.returncode))
|
| 53 |
+
def interrupt(signum,frame):raise SystemExit(128+signum)
|
| 54 |
+
for s in (signal.SIGINT,signal.SIGTERM):signal.signal(s,interrupt)
|
| 55 |
+
result['health_before']=request(9091,'/health')
|
| 56 |
+
assert not result['health_before']['active'] and result['health_before']['context_length']==65536
|
| 57 |
+
with socket.socket() as s:
|
| 58 |
+
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind(('127.0.0.1',19096))
|
| 59 |
+
(output/'health-before.json').write_text(json.dumps(result['health_before'],indent=2)+'\n')
|
| 60 |
+
try:
|
| 61 |
+
subprocess.run(['systemctl','--user','stop','mindnano-ling3.service'],check=True)
|
| 62 |
+
print('PHASE portable package 64K service, clean environment and unrelated cwd',flush=True)
|
| 63 |
+
with tempfile.TemporaryDirectory(prefix='ling3-release-empty-') as cwd, (output/'service.log').open('w') as log:
|
| 64 |
+
child=subprocess.Popen([str(package/'mindnano-infer'),'--context','64K','--no-console',
|
| 65 |
+
'--host','127.0.0.1','--port','19096','--log',str(output/'metrics.jsonl')],
|
| 66 |
+
cwd=cwd,env=env,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
| 67 |
+
try:
|
| 68 |
+
h=ready(19096,child);result['health']=h
|
| 69 |
+
assert h['context_length']==65536
|
| 70 |
+
fmt=h['weight_format']
|
| 71 |
+
assert fmt['source']=='self_contained_selective_w4_w8' and fmt['shared_stage']
|
| 72 |
+
assert fmt['linear_counts']=={'original_w4':5888,'calibrated_w4':18,'w8':91}
|
| 73 |
+
body={'model':'mindnano-ling3-tiny','temperature':0,'max_tokens':64,'cache_prompt':False,
|
| 74 |
+
'messages':[{'role':'user','content':'1'*107}]}
|
| 75 |
+
result['warm_request']=request(19096,'/v1/chat/completions',body)
|
| 76 |
+
result['runs']=[]
|
| 77 |
+
for i in range(3):
|
| 78 |
+
r=request(19096,'/v1/chat/completions',body)
|
| 79 |
+
assert r['usage']['prompt_tokens']==128 and r['usage']['completion_tokens']==64
|
| 80 |
+
assert r['mindnano_metrics']['cached_tokens']==0
|
| 81 |
+
result['runs'].append(r)
|
| 82 |
+
print(json.dumps(dict(run=i+1,metrics=r['mindnano_metrics'])),flush=True)
|
| 83 |
+
result['performance']={
|
| 84 |
+
'ttft_median_ms':statistics.median(r['mindnano_metrics']['ttft_ms'] for r in result['runs']),
|
| 85 |
+
'decode_median_tokens_per_second':statistics.median(r['mindnano_metrics']['decode_tokens_per_second'] for r in result['runs']),
|
| 86 |
+
'peak_rss_mib':max(r['mindnano_metrics']['peak_rss_mib'] for r in result['runs'])}
|
| 87 |
+
with (output/'api.json').open('w') as f:
|
| 88 |
+
subprocess.run(['/usr/bin/python3',str(root/'source/tests/portable_api_test.py'),
|
| 89 |
+
'--url','http://127.0.0.1:19096'],env=env,stdout=f,check=True)
|
| 90 |
+
print('PASS portable API / SSE / cancellation / request isolation',flush=True)
|
| 91 |
+
with (output/'prefix-cache.log').open('w') as f:
|
| 92 |
+
subprocess.run(['/usr/bin/python3',str(root/'source/tests/prefix_cache_api_test.py'),
|
| 93 |
+
'--url','http://127.0.0.1:19096','--output',str(output/'prefix-cache.json')],env=env,stdout=f,check=True)
|
| 94 |
+
print('PASS prefix cache / branching / namespace / streaming',flush=True)
|
| 95 |
+
finally:stop(child)
|
| 96 |
+
print('PHASE full Think / No-think equality against tested candidate',flush=True)
|
| 97 |
+
logits=output/'logits'
|
| 98 |
+
with (output/'probe.log').open('w') as log:
|
| 99 |
+
child=subprocess.Popen([str(root/'build/ling3-quant-loss-probe'),str(package/'ling3-tiny-w4.l3r'),
|
| 100 |
+
str(suite),str(logits)],env=env,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
| 101 |
+
try:
|
| 102 |
+
if child.wait()!=0:raise RuntimeError('full numerical probe failed')
|
| 103 |
+
finally:stop(child)
|
| 104 |
+
result['logit_equality']=[]
|
| 105 |
+
for case in json.loads(suite.read_text())['cases']:
|
| 106 |
+
name=case['id']+'.rknn.f32'
|
| 107 |
+
hashes=[]
|
| 108 |
+
for directory in (reference,logits):
|
| 109 |
+
file=directory/name
|
| 110 |
+
assert file.stat().st_size==len(case['teacher_ids'])*157184*4
|
| 111 |
+
with file.open('rb') as f:hashes.append(hashlib.file_digest(f,'sha256').hexdigest())
|
| 112 |
+
result['logit_equality'].append(dict(case=case['id'],sha256=hashes[1],bitwise_equal=hashes[0]==hashes[1]))
|
| 113 |
+
assert all(r['bitwise_equal'] for r in result['logit_equality']),result['logit_equality']
|
| 114 |
+
result['passed']=True
|
| 115 |
+
print('PASS all 4217 full-vocabulary logits bitwise identical to tested candidate',flush=True)
|
| 116 |
+
finally:
|
| 117 |
+
print('PHASE restoring original W4 / 64K service',flush=True)
|
| 118 |
+
state=subprocess.run(['systemctl','--user','show','mindnano-ling3.service','--property=LoadState','--value'],
|
| 119 |
+
capture_output=True,text=True).stdout.strip()
|
| 120 |
+
if state=='loaded':
|
| 121 |
+
subprocess.run(['systemctl','--user','start','mindnano-ling3.service'],check=True)
|
| 122 |
+
else:
|
| 123 |
+
subprocess.run(['systemd-run','--user','--unit=mindnano-ling3','--collect','--property=Restart=on-failure',
|
| 124 |
+
'--property=RestartSec=5','--working-directory='+str(deployment),str(deployment/'mindnano-infer'),
|
| 125 |
+
'--context','64K','--host','0.0.0.0','--port','9091','--no-console','--log',str(deployment/'metrics-live.jsonl')],check=True)
|
| 126 |
+
result['health_restored']=ready(9091)
|
| 127 |
+
assert result['health_restored']['context_length']==65536
|
| 128 |
+
(output/'results.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n')
|
| 129 |
+
print('PASS original service restored',flush=True)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
if __name__=='__main__':main()
|
tools/validate_weight_reclaim.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Compare deterministic API responses before/after source-weight reclamation."""
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import urllib.request
|
| 7 |
+
|
| 8 |
+
p = argparse.ArgumentParser()
|
| 9 |
+
p.add_argument("--url", required=True)
|
| 10 |
+
p.add_argument("--output", type=Path, required=True)
|
| 11 |
+
p.add_argument("--reference", type=Path)
|
| 12 |
+
p.add_argument("--boundaries", action="store_true", help="also check partial final prefill blocks")
|
| 13 |
+
a = p.parse_args()
|
| 14 |
+
client = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def call(path, body=None):
|
| 18 |
+
req = urllib.request.Request(a.url.rstrip("/") + path,
|
| 19 |
+
None if body is None else json.dumps(body).encode(),
|
| 20 |
+
{"Content-Type": "application/json"})
|
| 21 |
+
with client.open(req, timeout=180) as response:
|
| 22 |
+
return json.load(response)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
office = "请用中文列出三条会议纪要的写作建议。"
|
| 26 |
+
cases = [("office", office, 64), ("digits128_a", "1" * 107, 64),
|
| 27 |
+
("digits128_b", "1" * 107, 64), ("digits128_c", "1" * 107, 64),
|
| 28 |
+
("digits1024", "1" * 1003, 24), ("office_repeat", office, 64)]
|
| 29 |
+
if a.boundaries:
|
| 30 |
+
cases.extend((f"digits{count}", "1" * (count - 21), 16) for count in (129, 131, 255, 257))
|
| 31 |
+
cases.append(("office_after_boundaries", office, 64))
|
| 32 |
+
result = {"health_before": call("/health"), "cases": []}
|
| 33 |
+
for name, prompt, tokens in cases:
|
| 34 |
+
response = call("/v1/chat/completions", {
|
| 35 |
+
"model": "mindnano-ling3-tiny",
|
| 36 |
+
"messages": [{"role": "user", "content": prompt}],
|
| 37 |
+
"temperature": 0, "max_tokens": tokens, "cache_prompt": False})
|
| 38 |
+
result["cases"].append({"name": name, "response": response})
|
| 39 |
+
print(json.dumps({"case": name, "usage": response["usage"],
|
| 40 |
+
"metrics": response["mindnano_metrics"]}, ensure_ascii=False), flush=True)
|
| 41 |
+
result["health_after"] = call("/health")
|
| 42 |
+
a.output.parent.mkdir(parents=True, exist_ok=True)
|
| 43 |
+
a.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
| 44 |
+
assert result["cases"][0]["response"]["choices"] == result["cases"][-1]["response"]["choices"]
|
| 45 |
+
if a.reference:
|
| 46 |
+
ref = json.loads(a.reference.read_text())
|
| 47 |
+
assert len(ref["cases"]) == len(result["cases"])
|
| 48 |
+
for before, after in zip(ref["cases"], result["cases"]):
|
| 49 |
+
assert before["name"] == after["name"]
|
| 50 |
+
for field in ("choices", "usage"):
|
| 51 |
+
assert before["response"][field] == after["response"][field], (after["name"], field)
|
| 52 |
+
print("PASS: all response texts, finish reasons and token counts exactly match baseline")
|
tools/verify_release_archive.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Verify all release members and embedded SHA256SUMS without extracting the model."""
|
| 3 |
+
import argparse
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path, PurePosixPath
|
| 7 |
+
import tarfile
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
p=argparse.ArgumentParser(description=__doc__)
|
| 12 |
+
p.add_argument('archive',type=Path)
|
| 13 |
+
a=p.parse_args()
|
| 14 |
+
expected={};actual={};roots=set();metadata={}
|
| 15 |
+
with tarfile.open(a.archive,'r|gz') as tar:
|
| 16 |
+
for member in tar:
|
| 17 |
+
path=PurePosixPath(member.name)
|
| 18 |
+
assert not path.is_absolute() and '..' not in path.parts
|
| 19 |
+
roots.add(path.parts[0])
|
| 20 |
+
if member.isdir():continue
|
| 21 |
+
assert member.isfile() and len(path.parts)==2,member.name
|
| 22 |
+
name=path.name
|
| 23 |
+
assert name not in actual,'duplicate member: '+name
|
| 24 |
+
digest=hashlib.sha256();parts=[]
|
| 25 |
+
with tar.extractfile(member) as f:
|
| 26 |
+
while block:=f.read(8*1024*1024):
|
| 27 |
+
digest.update(block)
|
| 28 |
+
if name in ('SHA256SUMS','MODEL.json','VALIDATION.json'):parts.append(block)
|
| 29 |
+
actual[name]=digest.hexdigest()
|
| 30 |
+
if name=='SHA256SUMS':
|
| 31 |
+
for line in b''.join(parts).decode().splitlines():
|
| 32 |
+
checksum,filename=line.split(' ',1)
|
| 33 |
+
assert filename not in expected
|
| 34 |
+
expected[filename]=checksum
|
| 35 |
+
elif parts:metadata[name]=json.loads(b''.join(parts))
|
| 36 |
+
assert len(roots)==1
|
| 37 |
+
assert expected and set(expected)==set(actual)-{'SHA256SUMS'}
|
| 38 |
+
for name,checksum in expected.items():assert actual[name]==checksum,name
|
| 39 |
+
assert metadata['VALIDATION.json']['passed']
|
| 40 |
+
model = metadata['MODEL.json']
|
| 41 |
+
if model.get('mindnano_integration_version', 0) >= 5:
|
| 42 |
+
assert 'qa_cache_default' in model['capabilities']
|
| 43 |
+
assert 'resident_session_cache' in model['capabilities']
|
| 44 |
+
assert metadata['VALIDATION.json']['health']['mindnano_integration_version'] >= 5
|
| 45 |
+
else:
|
| 46 |
+
assert model['all_payload_sha256_valid']
|
| 47 |
+
print(json.dumps(dict(archive=str(a.archive),verified_files=len(expected),
|
| 48 |
+
all_member_sha256_valid=True,validation_passed=True,
|
| 49 |
+
performance=metadata['VALIDATION.json'].get('performance')),indent=2))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__=='__main__':main()
|