torch-ane 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- torch_ane/__init__.py +34 -0
- torch_ane/diffusion.py +166 -0
- torch_ane/llm/__init__.py +13 -0
- torch_ane/llm/_kv.py +98 -0
- torch_ane/llm/causal.py +317 -0
- torch_ane/llm/chunked.py +475 -0
- torch_ane/llm/gguf.py +15 -0
- torch_ane/passes/__init__.py +25 -0
- torch_ane/passes/composite.py +104 -0
- torch_ane/passes/export.py +298 -0
- torch_ane/passes/layout.py +309 -0
- torch_ane/passes/quant.py +202 -0
- torch_ane/runtime.py +407 -0
- torch_ane-0.1.0.dist-info/METADATA +167 -0
- torch_ane-0.1.0.dist-info/RECORD +18 -0
- torch_ane-0.1.0.dist-info/WHEEL +5 -0
- torch_ane-0.1.0.dist-info/licenses/LICENSE +21 -0
- torch_ane-0.1.0.dist-info/top_level.txt +1 -0
torch_ane/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""torch_ane — PyTorch-модели на Apple Neural Engine через Core AI (macOS 27+).
|
|
2
|
+
|
|
3
|
+
import torch_ane
|
|
4
|
+
|
|
5
|
+
m = torch_ane.compile(model.eval(), example_inputs=(x,)) # любой nn.Module -> ANE
|
|
6
|
+
y = m(x)
|
|
7
|
+
|
|
8
|
+
lm = torch_ane.CausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
|
|
9
|
+
ids = lm.generate(prompt_ids, max_new_tokens=64)
|
|
10
|
+
|
|
11
|
+
sd = torch_ane.StableDiffusion("segmind/tiny-sd")
|
|
12
|
+
image, timings = sd("a cute robot", steps=20, seed=0)
|
|
13
|
+
|
|
14
|
+
Нижний уровень — torch_ane.passes (проходы экспорта, раскладка Apple, палитры/int8,
|
|
15
|
+
составные операции). Сборки кешируются в torch_ane.CACHE_DIR (~/.cache/torch_ane,
|
|
16
|
+
переменная TORCH_ANE_CACHE). Библиотека не зависит от MLX.
|
|
17
|
+
"""
|
|
18
|
+
from . import passes
|
|
19
|
+
from .runtime import (CACHE_DIR, CompiledModule, available, cleanup_mpsgraph_tmp, clear_system_cache, compile,
|
|
20
|
+
compress_program, model_digest, prune_cache)
|
|
21
|
+
|
|
22
|
+
__all__ = ["compile", "CompiledModule", "available", "compress_program", "model_digest", "cleanup_mpsgraph_tmp",
|
|
23
|
+
"CACHE_DIR", "prune_cache", "clear_system_cache", "passes", "CausalLM", "ChunkedCausalLM", "StableDiffusion"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattr__(name):
|
|
27
|
+
# тяжёлые части (transformers, diffusers) — только по обращению
|
|
28
|
+
if name in ("CausalLM", "ChunkedCausalLM"):
|
|
29
|
+
from . import llm
|
|
30
|
+
return getattr(llm, name)
|
|
31
|
+
if name == "StableDiffusion":
|
|
32
|
+
from .diffusion import StableDiffusion
|
|
33
|
+
return StableDiffusion
|
|
34
|
+
raise AttributeError(f"module 'torch_ane' has no attribute {name!r}")
|
torch_ane/diffusion.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Stable Diffusion (diffusers) на Neural Engine: UNet, текстовый энкодер и VAE-декодер
|
|
2
|
+
через CompiledModule, расписание шагов и токенизатор — на CPU.
|
|
3
|
+
|
|
4
|
+
Главное для скорости — внимание в раскладке ANE (AttnProcessor): на ANE permute/reshape
|
|
5
|
+
больших тензоров — это копирование памяти, и внимание на 64x64 (4096 токенов) в обычной
|
|
6
|
+
записи было в 9 раз медленнее GPU. По рецепту Apple (BC1S, по головам, einsum без
|
|
7
|
+
транспонирований, softmax по ключам, куски запросов) UNet tiny-sd на ANE быстрее GPU.
|
|
8
|
+
|
|
9
|
+
sd = StableDiffusion("segmind/tiny-sd")
|
|
10
|
+
image, t = sd("a cute robot sitting on a desk", steps=20, seed=0)
|
|
11
|
+
"""
|
|
12
|
+
import hashlib
|
|
13
|
+
import inspect
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn as nn
|
|
18
|
+
|
|
19
|
+
Q_CHUNK = 512 # куски запросов во внимании при T > 1024
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AttnProcessor:
|
|
23
|
+
"""Обработчик внимания diffusers в раскладке ANE (как SPLIT_EINSUM у Apple):
|
|
24
|
+
Q и V — [B, C, 1, T], K — [B, T, 1, C] без транспонирования; по головам einsum
|
|
25
|
+
bchq,bkhc->bkhq, softmax по оси ключей, длинные запросы — кусками."""
|
|
26
|
+
|
|
27
|
+
def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None, temb=None, **kw):
|
|
28
|
+
res = hidden_states
|
|
29
|
+
shape4 = hidden_states.shape if hidden_states.ndim == 4 else None
|
|
30
|
+
if shape4 is not None:
|
|
31
|
+
b, c, hh, ww = shape4
|
|
32
|
+
hidden_states = hidden_states.view(b, c, hh * ww).transpose(1, 2)
|
|
33
|
+
if attn.group_norm is not None:
|
|
34
|
+
hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2)
|
|
35
|
+
ctx = hidden_states if encoder_hidden_states is None else encoder_hidden_states
|
|
36
|
+
if encoder_hidden_states is not None and attn.norm_cross:
|
|
37
|
+
ctx = attn.norm_encoder_hidden_states(ctx)
|
|
38
|
+
q, k, v = attn.to_q(hidden_states), attn.to_k(ctx), attn.to_v(ctx)
|
|
39
|
+
heads = attn.heads
|
|
40
|
+
d, tq = q.shape[-1] // heads, q.shape[1]
|
|
41
|
+
qb = q.transpose(1, 2).unsqueeze(2) # [B, C, 1, Tq]
|
|
42
|
+
kb = k.unsqueeze(2) # [B, Tk, 1, C]
|
|
43
|
+
vb = v.transpose(1, 2).unsqueeze(2) # [B, C, 1, Tk]
|
|
44
|
+
n = Q_CHUNK if tq > 1024 else tq
|
|
45
|
+
outs = []
|
|
46
|
+
for h in range(heads):
|
|
47
|
+
qh, kh, vh = qb[:, h * d:(h + 1) * d], kb[..., h * d:(h + 1) * d], vb[:, h * d:(h + 1) * d]
|
|
48
|
+
parts = []
|
|
49
|
+
for i in range(0, tq, n):
|
|
50
|
+
w = torch.einsum("bchq,bkhc->bkhq", qh[..., i:i + n], kh) * attn.scale
|
|
51
|
+
parts.append(torch.einsum("bkhq,bchk->bchq", w.softmax(dim=1), vh))
|
|
52
|
+
outs.append(torch.cat(parts, dim=3) if len(parts) > 1 else parts[0])
|
|
53
|
+
o = torch.cat(outs, dim=1).squeeze(2).transpose(1, 2) # [B, Tq, C]
|
|
54
|
+
o = attn.to_out[1](attn.to_out[0](o))
|
|
55
|
+
if shape4 is not None:
|
|
56
|
+
o = o.transpose(1, 2).reshape(shape4)
|
|
57
|
+
if attn.residual_connection:
|
|
58
|
+
o = o + res
|
|
59
|
+
return o / attn.rescale_output_factor
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# версия обработчика — в простом атрибуте обёрток, чтобы кеш сборки CompiledModule менялся вместе с ним
|
|
63
|
+
_ATTN_TAG = hashlib.sha256(inspect.getsource(AttnProcessor).encode()).hexdigest()[:8]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _UNet(nn.Module):
|
|
67
|
+
def __init__(self, unet):
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.u = unet
|
|
70
|
+
self.attn_tag = _ATTN_TAG
|
|
71
|
+
|
|
72
|
+
def forward(self, sample, timestep, encoder_hidden_states):
|
|
73
|
+
return self.u(sample, timestep, encoder_hidden_states, return_dict=False)[0]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _TextEncoder(nn.Module):
|
|
77
|
+
"""Энкодер CLIP без таблицы эмбеддингов: вход — эмбеддинги токенов+позиций (с CPU)."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, te):
|
|
80
|
+
super().__init__()
|
|
81
|
+
self.te = te
|
|
82
|
+
|
|
83
|
+
def forward(self, hidden):
|
|
84
|
+
h = self.te.encoder(inputs_embeds=hidden, attention_mask=None, is_causal=True).last_hidden_state
|
|
85
|
+
return self.te.final_layer_norm(h)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _VAEDecoder(nn.Module):
|
|
89
|
+
def __init__(self, vae):
|
|
90
|
+
super().__init__()
|
|
91
|
+
self.pqc, self.dec = vae.post_quant_conv, vae.decoder
|
|
92
|
+
self.attn_tag = _ATTN_TAG
|
|
93
|
+
|
|
94
|
+
def forward(self, z):
|
|
95
|
+
if self.pqc is not None:
|
|
96
|
+
z = self.pqc(z)
|
|
97
|
+
return self.dec(z)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class StableDiffusion:
|
|
101
|
+
"""Пайплайн Stable Diffusion из diffusers с UNet, текстовым энкодером и VAE-декодером
|
|
102
|
+
на заданном блоке Core AI ("ane" или "gpu"). Первая сборка программ —
|
|
103
|
+
минуты, дальше — из кеша."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, model_id="segmind/tiny-sd", device="ane", size=512):
|
|
106
|
+
from diffusers import DPMSolverMultistepScheduler, StableDiffusionPipeline
|
|
107
|
+
|
|
108
|
+
from .runtime import CompiledModule
|
|
109
|
+
self.device, self.size = device, size
|
|
110
|
+
pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float32, safety_checker=None,
|
|
111
|
+
requires_safety_checker=False)
|
|
112
|
+
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
|
|
113
|
+
pipe.set_progress_bar_config(disable=True)
|
|
114
|
+
pipe.unet.eval().set_attn_processor(AttnProcessor())
|
|
115
|
+
pipe.vae.eval().set_attn_processor(AttnProcessor())
|
|
116
|
+
te = pipe.text_encoder.eval()
|
|
117
|
+
self.t = {} # время по частям последней генерации
|
|
118
|
+
self._unet = CompiledModule(_UNet(pipe.unet).eval(), device=device, validate=False)
|
|
119
|
+
self._text = CompiledModule(_TextEncoder(te).eval(), device=device, validate=False)
|
|
120
|
+
self._vae = CompiledModule(_VAEDecoder(pipe.vae).eval(), device=device, validate=False)
|
|
121
|
+
sd = self
|
|
122
|
+
|
|
123
|
+
class UNetShim:
|
|
124
|
+
config, dtype = pipe.unet.config, pipe.unet.dtype
|
|
125
|
+
|
|
126
|
+
def __call__(self, sample, timestep, encoder_hidden_states=None, return_dict=True, **kw):
|
|
127
|
+
from diffusers.models.unets.unet_2d_condition import UNet2DConditionOutput
|
|
128
|
+
timestep = torch.as_tensor(timestep)
|
|
129
|
+
if timestep.dim() == 0:
|
|
130
|
+
timestep = timestep.float().expand(sample.shape[0])
|
|
131
|
+
a = time.perf_counter()
|
|
132
|
+
out = sd._unet(sample.float(), timestep.float(), encoder_hidden_states.float())
|
|
133
|
+
sd.t.setdefault("unet", []).append(time.perf_counter() - a)
|
|
134
|
+
return UNet2DConditionOutput(sample=out) if return_dict else (out,)
|
|
135
|
+
|
|
136
|
+
class TextShim:
|
|
137
|
+
config, dtype = te.config, te.dtype
|
|
138
|
+
|
|
139
|
+
def __call__(self, input_ids, attention_mask=None, output_hidden_states=False, **kw):
|
|
140
|
+
a = time.perf_counter()
|
|
141
|
+
with torch.no_grad(): # поиск по таблице эмбеддингов — на CPU
|
|
142
|
+
hidden = te.embeddings(input_ids=input_ids)
|
|
143
|
+
out = sd._text(hidden.float())
|
|
144
|
+
sd.t["text"] = sd.t.get("text", 0) + time.perf_counter() - a
|
|
145
|
+
return (out,)
|
|
146
|
+
|
|
147
|
+
def vae_decode(z, return_dict=True, generator=None, **kw):
|
|
148
|
+
from diffusers.models.autoencoders.vae import DecoderOutput
|
|
149
|
+
a = time.perf_counter()
|
|
150
|
+
out = sd._vae(z.float())
|
|
151
|
+
sd.t["vae"] = time.perf_counter() - a
|
|
152
|
+
return DecoderOutput(sample=out) if return_dict else (out,)
|
|
153
|
+
|
|
154
|
+
pipe.unet, pipe.text_encoder = UNetShim(), TextShim()
|
|
155
|
+
pipe.vae.decode = vae_decode
|
|
156
|
+
self.pipe = pipe
|
|
157
|
+
|
|
158
|
+
def __call__(self, prompt, steps=20, seed=None, negative_prompt="", guidance=7.5):
|
|
159
|
+
"""-> (PIL-картинка, {"text": с, "unet": [с на шаг], "vae": с, "total": с})."""
|
|
160
|
+
self.t = {}
|
|
161
|
+
g = torch.Generator("cpu").manual_seed(seed if seed is not None else int(time.time()))
|
|
162
|
+
a = time.perf_counter()
|
|
163
|
+
img = self.pipe(prompt, negative_prompt=negative_prompt, num_inference_steps=steps,
|
|
164
|
+
guidance_scale=guidance, height=self.size, width=self.size, generator=g).images[0]
|
|
165
|
+
self.t["total"] = time.perf_counter() - a
|
|
166
|
+
return img, self.t
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Языковые модели на Neural Engine.
|
|
2
|
+
|
|
3
|
+
CausalLM — модель из transformers целиком в одной программе (до ~1-3B):
|
|
4
|
+
prefill кусками, декод окнами, KV-кеш — состояния Core AI,
|
|
5
|
+
батч до 8 потоков за шаг (generate_batch);
|
|
6
|
+
ChunkedCausalLM — большие модели (24B) кусками по несколько слоёв, веса из GGUF,
|
|
7
|
+
палитры 4 бит / int8.
|
|
8
|
+
"""
|
|
9
|
+
from ._kv import fresh_rope
|
|
10
|
+
from .causal import CausalLM
|
|
11
|
+
from .chunked import ChunkedCausalLM
|
|
12
|
+
|
|
13
|
+
__all__ = ["CausalLM", "ChunkedCausalLM", "fresh_rope"]
|
torch_ane/llm/_kv.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Общее для LLM на ANE: KV-кеш — состояния Core AI (схема Apple: весь K одним
|
|
2
|
+
состоянием, весь V другим, запись куска — coreai.slice_update со смещением из входа),
|
|
3
|
+
слой кеша transformers поверх этих буферов и маска окна."""
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
from transformers.cache_utils import CacheLayerMixin
|
|
7
|
+
|
|
8
|
+
DECODE_Q = 8
|
|
9
|
+
MASK_NEG = -40000.0 # не -inf: ANE неверно считает softmax с -inf
|
|
10
|
+
|
|
11
|
+
_lib = torch.library.Library("torch_ane", "FRAGMENT")
|
|
12
|
+
_lib.define("kv_write(Tensor cache, Tensor new, int layer, Tensor step) -> Tensor")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@torch.library.impl(_lib, "kv_write", "CompositeExplicitAutograd")
|
|
16
|
+
def _kv_write(cache, new, layer, step):
|
|
17
|
+
"""cache [L, 1, kv, M, hd], new [1, 1, kv, C, hd] (ранг как у кеша — требование
|
|
18
|
+
slice_update): cache[layer, :, :, step:step+C] = new."""
|
|
19
|
+
out = cache.clone()
|
|
20
|
+
s = int(step.reshape(-1)[0])
|
|
21
|
+
out[layer:layer + 1, :, :, s:s + new.shape[3]] = new.to(out.dtype)
|
|
22
|
+
return out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@torch.library.register_fake("torch_ane::kv_write")
|
|
26
|
+
def _kv_write_fake(cache, new, layer, step):
|
|
27
|
+
return torch.empty_like(cache)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_lowering(conv):
|
|
31
|
+
from coreai._compiler.dialects import coreai
|
|
32
|
+
from coreai_torch._utils import get_operand
|
|
33
|
+
|
|
34
|
+
@conv.register_torch_lowering("torch_ane::kv_write.default")
|
|
35
|
+
def _(values_map, node, loc):
|
|
36
|
+
cache = get_operand(values_map, node, 0)
|
|
37
|
+
new = get_operand(values_map, node, 1)
|
|
38
|
+
layer = node.args[2]
|
|
39
|
+
step = get_operand(values_map, node, 3) # [1] int32
|
|
40
|
+
_, _, kv, n, hd = tuple(node.args[1].meta["val"].shape)
|
|
41
|
+
begin = coreai.concat(0, [[layer], [0], [0], step, [0]])
|
|
42
|
+
end = coreai.concat(0, [[layer + 1], [1], [kv], coreai.broadcasting_add(step, n), [hd]])
|
|
43
|
+
return coreai.slice_update(cache, begin, end, [1, 1, 1, 1, 1], new)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StateLayer(CacheLayerMixin):
|
|
47
|
+
"""Слой кеша transformers поверх общих буферов модуля-владельца."""
|
|
48
|
+
|
|
49
|
+
is_sliding = False
|
|
50
|
+
|
|
51
|
+
def __init__(self, owner, idx, step):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.owner, self.idx, self.step = owner, idx, step
|
|
54
|
+
self.is_initialized = True
|
|
55
|
+
|
|
56
|
+
def lazy_initialization(self, key_states, value_states):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def update(self, key_states, value_states, *args, **kwargs):
|
|
60
|
+
o, i = self.owner, self.idx
|
|
61
|
+
o.key_cache.copy_(torch.ops.torch_ane.kv_write(o.key_cache, key_states[None], i, self.step))
|
|
62
|
+
o.value_cache.copy_(torch.ops.torch_ane.kv_write(o.value_cache, value_states[None], i, self.step))
|
|
63
|
+
dt = key_states.dtype
|
|
64
|
+
return o.key_cache[i].to(dt), o.value_cache[i].to(dt) # [1, kv, M, hd]
|
|
65
|
+
|
|
66
|
+
def get_mask_sizes(self, query_length):
|
|
67
|
+
return self.owner.max_len, 0
|
|
68
|
+
|
|
69
|
+
def get_seq_length(self):
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
def get_max_length(self):
|
|
73
|
+
return self.owner.max_len
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def window_mask(start, n, q, max_len):
|
|
78
|
+
"""Строка j (позиция start+j) видит ключи [0, start+j]; строки паддинга — ключ 0."""
|
|
79
|
+
m = np.full((1, 1, q, max_len), MASK_NEG, np.float16)
|
|
80
|
+
for j in range(q):
|
|
81
|
+
m[0, 0, j, :(start + j + 1 if j < n else 1)] = 0
|
|
82
|
+
return m
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def fresh_rope(lm):
|
|
88
|
+
"""Пересоздать модуль RoPE из конфига во fp32. Если модель загружена или хоть раз
|
|
89
|
+
переведена в bf16/fp16, её inv_freq округлён — ключи в кеше от ANE-prefill
|
|
90
|
+
слегка расходились бы с RoPE, который mlx-lm считает при генерации точно."""
|
|
91
|
+
rope = getattr(getattr(lm, "model", None), "rotary_emb", None)
|
|
92
|
+
if rope is not None:
|
|
93
|
+
try:
|
|
94
|
+
lm.model.rotary_emb = type(rope)(config=lm.config)
|
|
95
|
+
except TypeError:
|
|
96
|
+
pass
|
|
97
|
+
return lm
|
|
98
|
+
|
torch_ane/llm/causal.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""LLM на Neural Engine с KV-кешем внутри модели — схема Apple (coreai-models,
|
|
2
|
+
iOS-экспорт): весь K одним состоянием Core AI, весь V другим, запись куска —
|
|
3
|
+
coreai.slice_update со смещением из входа. Состояния живут между вызовами и
|
|
4
|
+
передаются без копирования (время вызова
|
|
5
|
+
почти не растёт с размером кеша), в отличие от KV-кеша входом (~22 мс копирования
|
|
6
|
+
на шаг декода у Qwen2.5-0.5B).
|
|
7
|
+
|
|
8
|
+
Прежняя попытка (ANEChunkedPrefill: index_copy_ в отдельный буфер на каждый слой)
|
|
9
|
+
на ANE тихо считала неверно; с двумя общими буферами и slice_update — верно.
|
|
10
|
+
|
|
11
|
+
Как у Apple, две функции над одними состояниями: prefill кусками по PREFILL_Q
|
|
12
|
+
токенов и декод окнами по DECODE_Q (окно выровнено на кратное DECODE_Q, недописанный
|
|
13
|
+
хвост пересчитывается — запись тех же значений в те же позиции безвредна).
|
|
14
|
+
Модель transformers не переписывается: свой слой кеша (_StateLayer) пишет K/V в
|
|
15
|
+
буферы и отдаёт слою внимания срез на весь контекст, маска — 4D-вход.
|
|
16
|
+
"""
|
|
17
|
+
import asyncio
|
|
18
|
+
import contextlib
|
|
19
|
+
import hashlib
|
|
20
|
+
import io
|
|
21
|
+
import warnings
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import torch
|
|
25
|
+
from transformers.cache_utils import Cache
|
|
26
|
+
|
|
27
|
+
from .. import passes
|
|
28
|
+
from ..runtime import CACHE_DIR, compress_program, model_digest, prune_cache, touch_cache
|
|
29
|
+
from ._kv import DECODE_Q, MASK_NEG, StateLayer, fresh_rope, register_lowering, window_mask
|
|
30
|
+
|
|
31
|
+
PREFILL_Q = 256
|
|
32
|
+
LADDER = (512, 1024, 2048, 4096, 8192)
|
|
33
|
+
CALIB_TEXT = ("Объясни подробно, как устроен трансформер: внимание, MLP, нормализация. "
|
|
34
|
+
"Explain how attention works, step by step, with a short Python example.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _StateStep(torch.nn.Module):
|
|
38
|
+
"""(emb [1, Q, hidden], pos [1, Q], mask [1, 1, Q, M], step [1], pick [1, Q])
|
|
39
|
+
-> логиты одной строки [1, V] (строка выбирается one-hot вектором pick);
|
|
40
|
+
K/V куска пишутся в буферы key_cache/value_cache [L, 1, kv, M, hd].
|
|
41
|
+
|
|
42
|
+
Как у Apple: эмбеддинги приходят входом (поиск по таблице — снаружи), в графе
|
|
43
|
+
одна копия весов lm_head; логиты — только нужной позиции, а не всего окна.
|
|
44
|
+
Выбор строки — умножение на one-hot в fp16 (целочисленный индекс ANE не любит)."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, lm, max_len):
|
|
47
|
+
super().__init__()
|
|
48
|
+
cfg = lm.config.get_text_config()
|
|
49
|
+
self.lm, self.max_len = lm, max_len
|
|
50
|
+
self.n_layers = cfg.num_hidden_layers
|
|
51
|
+
kv = cfg.num_key_value_heads
|
|
52
|
+
hd = getattr(cfg, "head_dim", None) or cfg.hidden_size // cfg.num_attention_heads
|
|
53
|
+
shape = (self.n_layers, 1, kv, max_len, hd)
|
|
54
|
+
self.register_buffer("key_cache", torch.zeros(shape), persistent=False)
|
|
55
|
+
self.register_buffer("value_cache", torch.zeros(shape), persistent=False)
|
|
56
|
+
|
|
57
|
+
def forward(self, emb, pos, mask, step, pick):
|
|
58
|
+
dt = self.lm.lm_head.weight.dtype
|
|
59
|
+
cache = Cache(layers=[StateLayer(self, i, step) for i in range(self.n_layers)])
|
|
60
|
+
o = self.lm.model(inputs_embeds=emb.to(dt), position_ids=pos, attention_mask=mask.to(dt),
|
|
61
|
+
past_key_values=cache, use_cache=True)
|
|
62
|
+
if pick.shape[0] > 1: # батч потоков: pick [P, Q] -> логиты P строк [P, V]
|
|
63
|
+
return self.lm.lm_head(torch.matmul(pick.to(dt), o.last_hidden_state[0]))
|
|
64
|
+
h = (pick.to(dt)[:, :, None] * o.last_hidden_state).sum(1) # [1, hidden]
|
|
65
|
+
return self.lm.lm_head(h)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CausalLM:
|
|
69
|
+
"""Causal LM из transformers целиком на ANE: prefill кусками, декод окнами, KV-кеш —
|
|
70
|
+
состояния Core AI. Первая сборка — минуты, дальше программа из кеша.
|
|
71
|
+
|
|
72
|
+
lm = CausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", batch=True)
|
|
73
|
+
tokens = lm.generate(prompt_ids, max_new_tokens=64)
|
|
74
|
+
outs = lm.generate_batch([ids1, ids2, ids3], max_new_tokens=64) # до 8 потоков за шаг
|
|
75
|
+
|
|
76
|
+
max_len — наибольший контекст; compress — "lut4"/"lut4g32" (палитры Apple) или сжатие
|
|
77
|
+
готовой программы ("int8"...); composite — составные операции Core AI ("sdpa", "rope");
|
|
78
|
+
calib_ids — токены настоящего текста для калибровки масштаба fp16; batch — собрать
|
|
79
|
+
функции для generate_batch; mlp_chunk — MLP кусками под внутреннюю память ANE."""
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_pretrained(cls, model_id, **kw):
|
|
83
|
+
"""Модель и (для калибровки) токенизатор из transformers по имени или пути."""
|
|
84
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
85
|
+
hf = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.float32).eval()
|
|
86
|
+
if "calib_ids" not in kw:
|
|
87
|
+
try:
|
|
88
|
+
tok = AutoTokenizer.from_pretrained(model_id)
|
|
89
|
+
kw["calib_ids"] = tok(CALIB_TEXT)["input_ids"][:128]
|
|
90
|
+
except Exception: # noqa: BLE001 — без токенизатора: фиксированные токены
|
|
91
|
+
pass
|
|
92
|
+
return cls(hf, **kw)
|
|
93
|
+
|
|
94
|
+
def __init__(self, hf_lm, max_len=2048, device="ane", prefill_q=PREFILL_Q, compress=None,
|
|
95
|
+
ladder=LADDER, composite=(), calib_ids=None, batch=False, mlp_chunk=True):
|
|
96
|
+
import copy
|
|
97
|
+
|
|
98
|
+
from ..runtime import _device
|
|
99
|
+
device = _device(device)
|
|
100
|
+
self.lm = fresh_rope(copy.deepcopy(hf_lm).float()).eval()
|
|
101
|
+
self._embed = self.lm.get_input_embeddings()
|
|
102
|
+
# мелкие активации MLP/V не тонут в субнормальных fp16 на ANE (до подмены Linear свёртками);
|
|
103
|
+
# масштаб — свой у каждой пары проекций, по калибровке (общий 2^8 переполнял fp16 на
|
|
104
|
+
# массивных активациях: Qwen2.5 на ANE зацикливался, как и Devstral)
|
|
105
|
+
vocab = self.lm.get_input_embeddings().num_embeddings
|
|
106
|
+
cal = torch.tensor([list(calib_ids) if calib_ids is not None
|
|
107
|
+
else [1 % vocab] + [i % vocab for i in range(1000, 1095)]])
|
|
108
|
+
k_for, _ = passes.calibrate_rescale(self.lm, lambda: self.lm(input_ids=cal).logits)
|
|
109
|
+
passes.rescale_for_fp16(self.lm, k_for=k_for)
|
|
110
|
+
# MLP кусками скрытого измерения под внутреннюю память ANE (passes.chunk_mlp)
|
|
111
|
+
self.mlp_chunk = mlp_chunk and passes.chunk_mlp(self.lm)
|
|
112
|
+
if compress and compress.startswith("lut"): # "lut4" / "lut4g32": палитры по рецепту Apple
|
|
113
|
+
bits, _, g = compress[3:].partition("g")
|
|
114
|
+
passes.swap_linear(self.lm.model) # проекции — свёртки 1x1 (lm_head остаётся Linear)
|
|
115
|
+
with contextlib.redirect_stdout(io.StringIO()), warnings.catch_warnings():
|
|
116
|
+
warnings.simplefilter("ignore")
|
|
117
|
+
self.lm = passes.palettize_lut(self.lm, int(bits), int(g or 8))
|
|
118
|
+
# prefill_q: куски prefill крупнее окна декода — каждый кусок заново читает все
|
|
119
|
+
# веса, мелкие куски упираются в память, а не в вычисления
|
|
120
|
+
self.max_len, self.device, self.prefill_q, self.compress = max_len, device, prefill_q, compress
|
|
121
|
+
self.batch = batch # функции "batch": окно декода из токенов разных потоков (generate_batch)
|
|
122
|
+
# лестница контекстов (как у Apple): внимание на ANE стоит пропорционально
|
|
123
|
+
# длине кеша, короткий разговор идёт функциями с маленьким кешем
|
|
124
|
+
self.ladder = tuple(b for b in ladder if b < max_len and b >= prefill_q) + (max_len,)
|
|
125
|
+
passes.swap_rmsnorm(self.lm)
|
|
126
|
+
# составные SDPA/RoPE Core AI (composite=("sdpa", "rope")) — как в экспорте Apple под macOS
|
|
127
|
+
self.composite = tuple(composite)
|
|
128
|
+
self._extra_specs = passes.use_composite_ops(self.lm, self.composite) if self.composite else []
|
|
129
|
+
self.loop = asyncio.new_event_loop()
|
|
130
|
+
import coreai.runtime as rt
|
|
131
|
+
self._rt = rt
|
|
132
|
+
self._load(self._build())
|
|
133
|
+
self.reset()
|
|
134
|
+
|
|
135
|
+
def _entries(self):
|
|
136
|
+
kinds = ("prefill", "decode") + (("batch",) if self.batch else ())
|
|
137
|
+
return [(kind, M) for M in self.ladder for kind in kinds]
|
|
138
|
+
|
|
139
|
+
def _build(self):
|
|
140
|
+
"""Все функции лестницы — в одной программе Core AI: веса общие."""
|
|
141
|
+
from coreai_torch import ExternalizeSpec, TorchConverter
|
|
142
|
+
from coreai_torch.composite_ops import RMSNormImpl
|
|
143
|
+
|
|
144
|
+
tag = hashlib.sha256((f"{self.prefill_q}|{self.ladder}|{self.compress}|{self.composite}|{passes.PASSES_VERSION}"
|
|
145
|
+
+ ("|batch" if self.batch else "") + ("|mlpchunk" if self.mlp_chunk else "")).encode()).hexdigest()[:8]
|
|
146
|
+
path = CACHE_DIR / f"{model_digest(self.lm)}_state_{tag}_v5.aimodel"
|
|
147
|
+
if path.exists():
|
|
148
|
+
touch_cache(path)
|
|
149
|
+
return path
|
|
150
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
hid = self.lm.config.get_text_config().hidden_size
|
|
152
|
+
with warnings.catch_warnings(), torch.no_grad(), contextlib.redirect_stdout(io.StringIO()):
|
|
153
|
+
warnings.simplefilter("ignore")
|
|
154
|
+
conv = TorchConverter()
|
|
155
|
+
register_lowering(conv)
|
|
156
|
+
mods = {}
|
|
157
|
+
for kind, M in self._entries():
|
|
158
|
+
q = self.prefill_q if kind == "prefill" else DECODE_Q
|
|
159
|
+
m = mods.setdefault(M, _StateStep(self.lm, M).eval()) # модули делят self.lm — и веса
|
|
160
|
+
# позиции — int16: ANE не знает int32, и каст int32 -> float в RoPE уходит с ANE
|
|
161
|
+
# (у Apple position_ids — uint16 по той же причине)
|
|
162
|
+
args = (torch.zeros(1, q, hid, dtype=torch.float16), torch.arange(q, dtype=torch.int16)[None],
|
|
163
|
+
torch.from_numpy(window_mask(0, q, q, M)), torch.zeros(1, dtype=torch.int32),
|
|
164
|
+
torch.zeros(q if kind == "batch" else 1, q, dtype=torch.float16))
|
|
165
|
+
conv.add_pytorch_module(
|
|
166
|
+
m, export_fn=passes.export_for_ane(m, args), entrypoint_name=f"{kind}_{M}",
|
|
167
|
+
externalize_modules=[ExternalizeSpec(target_class=RMSNormImpl, composite_op_name="rms_norm",
|
|
168
|
+
composite_attrs=["axes", "eps"])] + self._extra_specs)
|
|
169
|
+
prog = conv.to_coreai()
|
|
170
|
+
if self.compress:
|
|
171
|
+
# lut*: проекции уже сжаты палитрами, int8 добирает остальное (lm_head)
|
|
172
|
+
prog = compress_program(prog, "int8" if self.compress.startswith("lut") else self.compress)
|
|
173
|
+
# hardware constraints (как у Apple): состояния — IOSurface, память, которую
|
|
174
|
+
# ANE читает напрямую
|
|
175
|
+
from coreai.authoring.types import AllocationType, HardwareConstraints
|
|
176
|
+
hc = HardwareConstraints(AllocationType.IOSurface, alignments=(1,) * 6, interleave=(1,) * 5)
|
|
177
|
+
for kind, M in self._entries():
|
|
178
|
+
prog.set_hardware_constraints(f"{kind}_{M}", {"b_key_cache": hc, "b_value_cache": hc})
|
|
179
|
+
prog.save_asset(path)
|
|
180
|
+
prune_cache(keep=(path,))
|
|
181
|
+
return path
|
|
182
|
+
|
|
183
|
+
def _load(self, path):
|
|
184
|
+
rt = self._rt
|
|
185
|
+
model = self.loop.run_until_complete(rt.AIModel.load(
|
|
186
|
+
path, rt.SpecializationOptions.from_preferred_compute_unit_kind(getattr(rt.ComputeUnitKind, self.device)())))
|
|
187
|
+
self.fn = {(kind, M): model.load_function(f"{kind}_{M}") for kind, M in self._entries()}
|
|
188
|
+
|
|
189
|
+
def reset(self, M=None):
|
|
190
|
+
"""Новые буферы состояний ступени M (по умолчанию — младшей)."""
|
|
191
|
+
self.M = M or self.ladder[0]
|
|
192
|
+
d = self.fn[("decode", self.M)].desc
|
|
193
|
+
self.state = {n: self._rt.NDArray.from_descriptor(d.state_descriptor(n)) for n in d.state_names}
|
|
194
|
+
self.pos = 0
|
|
195
|
+
|
|
196
|
+
def _run(self, kind, tokens, start):
|
|
197
|
+
rt = self._rt
|
|
198
|
+
q = self.prefill_q if kind == "prefill" else DECODE_Q
|
|
199
|
+
n = len(tokens)
|
|
200
|
+
ids = torch.zeros(1, q, dtype=torch.long)
|
|
201
|
+
ids[0, :n] = torch.tensor(tokens)
|
|
202
|
+
with torch.no_grad(): # поиск по таблице эмбеддингов — на CPU (как gather_embeddings у Apple)
|
|
203
|
+
emb = self._embed(ids).half().numpy()
|
|
204
|
+
pick = np.zeros((1, q), np.float16)
|
|
205
|
+
pick[0, n - 1] = 1
|
|
206
|
+
fn = self.fn[(kind, self.M)]
|
|
207
|
+
d = fn.desc
|
|
208
|
+
vals = (emb, np.arange(start, start + q, dtype=np.int16)[None], window_mask(start, n, q, self.M),
|
|
209
|
+
np.array([start], np.int32), pick)
|
|
210
|
+
out = self.loop.run_until_complete(fn({k: rt.NDArray(v) for k, v in zip(d.input_names, vals)},
|
|
211
|
+
state=self.state))
|
|
212
|
+
return np.array(out[d.output_names[0]].numpy())[0].astype(np.float32)
|
|
213
|
+
|
|
214
|
+
def feed(self, tokens):
|
|
215
|
+
"""Дописать токены в кеш (после уже обработанных) -> логиты последнего."""
|
|
216
|
+
toks = list(tokens)
|
|
217
|
+
self._hist = getattr(self, "_hist", [])[:self.pos] + toks
|
|
218
|
+
end = len(self._hist)
|
|
219
|
+
if end > min(self.max_len, 32767): # позиции — int16
|
|
220
|
+
raise ValueError(f"контекст длиннее {self.max_len}")
|
|
221
|
+
if end > self.M: # не влезает в ступень: следующая ступень, история пересчитывается заново
|
|
222
|
+
self.reset(next(b for b in self.ladder if b >= end))
|
|
223
|
+
Q, logits = self.prefill_q, None
|
|
224
|
+
while end - self.pos > DECODE_Q: # куски prefill, последний — неполный (паддинг под маской)
|
|
225
|
+
start = min(self.pos, self.M - Q) # кусок целиком в кеше: у края сдвигается назад
|
|
226
|
+
stop = min(start + Q, end)
|
|
227
|
+
logits = self._run("prefill", self._hist[start:stop], start)
|
|
228
|
+
self.pos = stop
|
|
229
|
+
while self.pos < end: # хвост окнами декода, выровненными на DECODE_Q
|
|
230
|
+
start = (self.pos // DECODE_Q) * DECODE_Q
|
|
231
|
+
stop = min(start + DECODE_Q, end)
|
|
232
|
+
logits = self._run("decode", self._hist[start:stop], start)
|
|
233
|
+
self.pos = stop
|
|
234
|
+
return logits
|
|
235
|
+
|
|
236
|
+
def generate(self, prompt, max_new_tokens=32, eos=None):
|
|
237
|
+
"""Жадная генерация с нуля: prompt — список id -> новые id (с eos, если встретился)."""
|
|
238
|
+
self.reset()
|
|
239
|
+
self._hist = []
|
|
240
|
+
out = []
|
|
241
|
+
logits = self.feed(prompt)
|
|
242
|
+
for _ in range(max_new_tokens):
|
|
243
|
+
tok = int(logits.argmax())
|
|
244
|
+
out.append(tok)
|
|
245
|
+
if tok == eos or len(out) == max_new_tokens:
|
|
246
|
+
break
|
|
247
|
+
logits = self.feed([tok])
|
|
248
|
+
return out
|
|
249
|
+
|
|
250
|
+
def _call(self, kind, tokens, pos, mask, step, pick):
|
|
251
|
+
"""Один вызов функции: токены по строкам окна, свои позиции и маска у каждой строки."""
|
|
252
|
+
rt = self._rt
|
|
253
|
+
q = self.prefill_q if kind == "prefill" else DECODE_Q
|
|
254
|
+
ids = torch.zeros(1, q, dtype=torch.long)
|
|
255
|
+
ids[0, :len(tokens)] = torch.tensor(tokens)
|
|
256
|
+
with torch.no_grad():
|
|
257
|
+
emb = self._embed(ids).half().numpy()
|
|
258
|
+
p = np.zeros((1, q), np.int16)
|
|
259
|
+
p[0, :len(pos)] = pos
|
|
260
|
+
fn = self.fn[(kind, self.M)]
|
|
261
|
+
d = fn.desc
|
|
262
|
+
vals = (emb, p, mask, np.array([step], np.int32), pick)
|
|
263
|
+
out = self.loop.run_until_complete(fn({k: rt.NDArray(v) for k, v in zip(d.input_names, vals)},
|
|
264
|
+
state=self.state))
|
|
265
|
+
return np.array(out[d.output_names[0]].numpy()).astype(np.float32)
|
|
266
|
+
|
|
267
|
+
def generate_batch(self, prompts, max_new_tokens=32):
|
|
268
|
+
"""Жадная генерация до DECODE_Q потоков сразу (нужен batch=True). Шаг декода — один
|
|
269
|
+
вызов ANE: строка j окна — очередной токен потока j со своей позицией; K/V всех
|
|
270
|
+
потоков лежат в общем буфере вперемешку, маска строки открывает только столбцы её
|
|
271
|
+
потока. Веса читаются раз на шаг, поэтому шаг стоит почти как у одного потока."""
|
|
272
|
+
if not self.batch:
|
|
273
|
+
raise ValueError("generate_batch: собери модель с batch=True")
|
|
274
|
+
B, Q, PQ = len(prompts), DECODE_Q, self.prefill_q
|
|
275
|
+
if not 1 <= B <= Q:
|
|
276
|
+
raise ValueError(f"потоков от 1 до {Q}")
|
|
277
|
+
need = sum(len(p) for p in prompts) + B * max_new_tokens + PQ
|
|
278
|
+
if need > self.max_len:
|
|
279
|
+
raise ValueError(f"не влезает в кеш: нужно {need} столбцов, max_len={self.max_len}")
|
|
280
|
+
self.reset(next(b for b in self.ladder if b >= need))
|
|
281
|
+
M, cur = self.M, 0
|
|
282
|
+
own = np.zeros((B, M), bool) # какие столбцы кеша принадлежат потоку
|
|
283
|
+
logits = []
|
|
284
|
+
for s, prompt in enumerate(prompts): # prefill потоков по очереди, каждый — в свой отрезок кеша
|
|
285
|
+
done = 0
|
|
286
|
+
while done < len(prompt):
|
|
287
|
+
chunk = prompt[done:done + PQ]
|
|
288
|
+
n = len(chunk)
|
|
289
|
+
m = np.full((1, 1, PQ, M), MASK_NEG, np.float16)
|
|
290
|
+
m[0, 0, :, own[s]] = 0
|
|
291
|
+
for j in range(PQ): # причинно внутри куска; строки паддинга видят себя
|
|
292
|
+
m[0, 0, j, cur:cur + (j + 1 if j < n else 0)] = 0
|
|
293
|
+
m[0, 0, j, cur + j] = 0
|
|
294
|
+
pick = np.zeros((1, PQ), np.float16)
|
|
295
|
+
pick[0, n - 1] = 1
|
|
296
|
+
lg = self._call("prefill", chunk, np.arange(done, done + n), m, cur, pick)[0]
|
|
297
|
+
own[s, cur:cur + n] = True
|
|
298
|
+
cur += n
|
|
299
|
+
done += n
|
|
300
|
+
logits.append(lg)
|
|
301
|
+
outs = [[int(lg.argmax())] for lg in logits]
|
|
302
|
+
pos = [len(p) for p in prompts]
|
|
303
|
+
mask = np.full((1, 1, Q, M), MASK_NEG, np.float16)
|
|
304
|
+
pick = np.eye(Q, dtype=np.float16)
|
|
305
|
+
for _ in range(max_new_tokens - 1):
|
|
306
|
+
mask[:] = MASK_NEG
|
|
307
|
+
for j in range(Q):
|
|
308
|
+
if j < B:
|
|
309
|
+
mask[0, 0, j, own[j]] = 0
|
|
310
|
+
mask[0, 0, j, cur + j] = 0
|
|
311
|
+
lg = self._call("batch", [o[-1] for o in outs], pos, mask, cur, pick)
|
|
312
|
+
own[np.arange(B), cur + np.arange(B)] = True
|
|
313
|
+
cur += B
|
|
314
|
+
for j in range(B):
|
|
315
|
+
outs[j].append(int(lg[j].argmax()))
|
|
316
|
+
pos[j] += 1
|
|
317
|
+
return outs
|