ollada 0.3.7__py3-none-win_amd64.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.
ollada/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ollada — the shelf for open-source diffusion language models."""
2
+
3
+ __version__ = "0.3.7"
@@ -0,0 +1,33 @@
1
+ """Family adapter registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ollada.adapters.base import AdapterError, BaseAdapter, GenResult
6
+ from ollada.recipes import Recipe
7
+
8
+ __all__ = ["AdapterError", "BaseAdapter", "GenResult", "create_adapter"]
9
+
10
+
11
+ def create_adapter(recipe: Recipe) -> BaseAdapter:
12
+ """Import lazily so the CLI works on machines without torch installed."""
13
+ if recipe.family == "dream":
14
+ from ollada.adapters.dream import DreamAdapter
15
+
16
+ return DreamAdapter(recipe)
17
+ if recipe.family == "sdar":
18
+ from ollada.adapters.sdar import SdarAdapter
19
+
20
+ return SdarAdapter(recipe)
21
+ if recipe.family == "nemotron":
22
+ # Dormant: Nemotron is the first planned post-v0.1 addition. Its
23
+ # adapter stays in the tree; it joins the shelf (catalog) later,
24
+ # together with the isolated-worker build its transformers>=5
25
+ # requirement forces (PLAN.md section 5, decision record).
26
+ from ollada.adapters.nemotron import NemotronAdapter
27
+
28
+ return NemotronAdapter(recipe)
29
+ if recipe.family == "diffcoder":
30
+ from ollada.adapters.diffcoder import DiffCoderAdapter
31
+
32
+ return DiffCoderAdapter(recipe)
33
+ raise AdapterError(f"no adapter for family '{recipe.family}' (model {recipe.name})")
@@ -0,0 +1,130 @@
1
+ """Adapter interface — one adapter per model FAMILY.
2
+
3
+ An adapter hides everything family-specific (how the model loads, what its
4
+ generate() is called, which knobs it takes) behind one interface, so the
5
+ daemon and CLI never see a family's custom code paths directly.
6
+
7
+ IMPORTANT: an adapter existing is NOT support. Support means the recipe's
8
+ verification block is filled by a real-load run on real GPU hardware
9
+ (PLAN.md section 7). Until then each adapter is unverified.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from dataclasses import dataclass
16
+
17
+ from ollada.recipes import Recipe
18
+
19
+
20
+ @dataclass
21
+ class GenResult:
22
+ text: str
23
+ tokens: int
24
+ seconds: float
25
+ nfe: int | None = None # number of function evaluations, when the family reports it
26
+ # Worker-reported measurement (worker path only; in-process loads use
27
+ # measurements.capture_and_record instead). None means "not measured".
28
+ peak_gb: float | None = None
29
+ hardware: str | None = None
30
+
31
+
32
+ class AdapterError(RuntimeError):
33
+ pass
34
+
35
+
36
+ class BaseAdapter:
37
+ family = "base"
38
+ # Watch mode: an adapter that can report per-step canvas state
39
+ # during generation sets this True and accepts on_step in chat().
40
+ # The manager forwards a step callback only to supporting
41
+ # adapters; everyone else is called exactly as before.
42
+ supports_watch = False
43
+
44
+ def __init__(self, recipe: Recipe):
45
+ self.recipe = recipe
46
+ self.model = None
47
+ self.tokenizer = None
48
+
49
+ # -- lifecycle ---------------------------------------------------------
50
+ def load(self) -> None:
51
+ raise NotImplementedError
52
+
53
+ def unload(self) -> None:
54
+ self.model = None
55
+ self.tokenizer = None
56
+ try:
57
+ import torch
58
+
59
+ if torch.cuda.is_available():
60
+ torch.cuda.empty_cache()
61
+ except Exception:
62
+ pass
63
+
64
+ @property
65
+ def loaded(self) -> bool:
66
+ return self.model is not None
67
+
68
+ # -- generation --------------------------------------------------------
69
+ def chat(
70
+ self, messages: list[dict], overrides: dict | None = None, on_step=None
71
+ ) -> GenResult:
72
+ raise NotImplementedError
73
+
74
+ # -- helpers ------------------------------------------------------------
75
+ def _params(self, overrides: dict | None) -> dict:
76
+ params = dict(self.recipe.generate)
77
+ if overrides:
78
+ params.update(overrides)
79
+ return params
80
+
81
+ def _prompt_ids(self, messages: list[dict]):
82
+ """Tokenize a chat history, preferring the tokenizer's chat template."""
83
+ from collections.abc import Mapping
84
+
85
+ tok = self.tokenizer
86
+ if getattr(tok, "chat_template", None):
87
+ result = tok.apply_chat_template(
88
+ messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
89
+ )
90
+ # BatchEncoding is a dict subclass on transformers 4.x but a
91
+ # UserDict (Mapping, NOT a dict) on 5.x — an isinstance(dict)
92
+ # check silently passes it through whole on 5.x, and the model's
93
+ # generate then dies on `.shape` (DiffCoder T4 run, 2026-10-21).
94
+ if isinstance(result, Mapping):
95
+ result = result["input_ids"]
96
+ return result
97
+ text = ""
98
+ for msg in messages:
99
+ text += f"{msg['role']}: {msg['content']}\n"
100
+ text += "assistant:"
101
+ return tok(text, return_tensors="pt").input_ids
102
+
103
+ def _finish(self, out_ids, prompt_len: int, started: float, nfe: int | None = None) -> GenResult:
104
+ new_ids = out_ids[0][prompt_len:]
105
+ text = self.tokenizer.decode(new_ids, skip_special_tokens=True)
106
+ return GenResult(
107
+ text=text.strip(),
108
+ tokens=int(new_ids.shape[0]),
109
+ seconds=time.time() - started,
110
+ nfe=nfe,
111
+ )
112
+
113
+ def _require_loaded(self):
114
+ if not self.loaded:
115
+ raise AdapterError(f"{self.recipe.name} is not loaded")
116
+
117
+
118
+ def quantization_config(quant: str):
119
+ """Build a BitsAndBytesConfig for int8/int4, else None.
120
+
121
+ Never pass load_in_8bit as a raw from_pretrained kwarg — it leaks into
122
+ some families' custom __init__ and crashes (npc-engine, 2026-10-20).
123
+ """
124
+ if quant not in ("int8", "int4"):
125
+ return None
126
+ from transformers import BitsAndBytesConfig
127
+
128
+ if quant == "int8":
129
+ return BitsAndBytesConfig(load_in_8bit=True)
130
+ return BitsAndBytesConfig(load_in_4bit=True)
@@ -0,0 +1,103 @@
1
+ """Stable-DiffCoder family adapter (ByteDance-Seed).
2
+
3
+ Real signature ( modeling_stable_diffcoder.py, read 2026-10-21 ):
4
+ generate(input_ids=None, generation_config=None, **kwargs) -> plain tensor
5
+ kwargs forward to generate_block(steps=128, gen_length=128, block_length=4,
6
+ temperature=0.0, remasking='low_confidence', tokenizer=None,
7
+ mask_id=5, threshold=0.95, shift=False, eos_id=None)
8
+ The model card's canonical example passes tokenizer=tokenizer,
9
+ threshold=None and eos_id=tokenizer.eos_token_id explicitly — the
10
+ generate_block defaults differ (threshold 0.95, no EOS truncation), so we
11
+ pass all three. Assertions in generate_block: gen_length % block_length == 0
12
+ and steps % (gen_length // block_length) == 0 (recipe defaults satisfy both:
13
+ 256 % 4 == 0, 128 % 64 == 0). Batch size must be 1.
14
+
15
+ The model's config was saved with transformers 5.3.0 — see the recipe pin.
16
+
17
+ Load repairs applied by this adapter:
18
+ 1. generate_block builds its block-causal mask with dtype=torch.bfloat16
19
+ hardcoded, while the recipe loads fp16 on pre-Ampere GPUs (T4) —
20
+ transformers 5.x SDPA rejects a float mask whose dtype differs from the
21
+ query dtype (RuntimeError, T4 run 2026-10-21). The adapter wraps
22
+ _make_block_causal_mask so the mask follows the model's dtype. The mask
23
+ is additive (0 / -inf), so fp16 loses nothing.
24
+
25
+ UNVERIFIED in ollada until a real-load gate passes on a T4.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import time
31
+
32
+ import torch
33
+ from transformers import AutoModelForCausalLM, AutoTokenizer
34
+
35
+ from ollada.adapters.base import BaseAdapter, quantization_config
36
+
37
+ _DTYPES = {"float16": torch.float16, "bfloat16": torch.bfloat16}
38
+
39
+
40
+ def _align_mask_dtype(model) -> bool:
41
+ """Wrap _make_block_causal_mask so its output dtype follows the model.
42
+
43
+ Returns True when the wrapper was installed. See the module docstring
44
+ (repair 1) for why this exists.
45
+ """
46
+ orig = getattr(model, "_make_block_causal_mask", None)
47
+ if orig is None:
48
+ return False
49
+ target = model.dtype
50
+
51
+ def patched(*args, **kwargs):
52
+ kwargs["dtype"] = target
53
+ return orig(*args, **kwargs)
54
+
55
+ model._make_block_causal_mask = patched
56
+ return True
57
+
58
+
59
+ class DiffCoderAdapter(BaseAdapter):
60
+ family = "diffcoder"
61
+
62
+ def load(self) -> None:
63
+ recipe = self.recipe
64
+ notes: list[str] = []
65
+ self.tokenizer = AutoTokenizer.from_pretrained(recipe.source, trust_remote_code=True)
66
+ kwargs = {
67
+ "trust_remote_code": True,
68
+ "dtype": _DTYPES.get(recipe.dtype, torch.float16),
69
+ }
70
+ qc = quantization_config(recipe.quant_default)
71
+ if qc is not None:
72
+ kwargs["quantization_config"] = qc
73
+ kwargs["device_map"] = "auto"
74
+ self.model = AutoModelForCausalLM.from_pretrained(recipe.source, **kwargs)
75
+ if qc is None and torch.cuda.is_available():
76
+ self.model = self.model.cuda()
77
+ self.model.eval()
78
+ if _align_mask_dtype(self.model):
79
+ notes.append(f"block mask dtype aligned to model dtype ({self.model.dtype})")
80
+ self.load_notes = notes
81
+
82
+ @torch.no_grad()
83
+ def chat(self, messages, overrides=None):
84
+ self._require_loaded()
85
+ params = self._params(overrides)
86
+ input_ids = self._prompt_ids(messages).to(self.model.device)
87
+ started = time.time()
88
+ threshold = params.get("threshold", None) # card example: None (fixed schedule)
89
+ out = self.model.generate(
90
+ input_ids,
91
+ steps=int(params.get("steps", 128)),
92
+ gen_length=int(params.get("gen_length", 256)),
93
+ block_length=int(params.get("block_length", 4)),
94
+ temperature=float(params.get("temperature", 0.0)),
95
+ remasking=str(params.get("remasking", "low_confidence")),
96
+ tokenizer=self.tokenizer,
97
+ shift=False,
98
+ threshold=None if threshold is None else float(threshold),
99
+ eos_id=self.tokenizer.eos_token_id,
100
+ )
101
+ # generate() returns a plain tensor (not a GenerateOutput).
102
+ ids = out.sequences if hasattr(out, "sequences") else out
103
+ return self._finish(ids, input_ids.shape[1], started)
@@ -0,0 +1,59 @@
1
+ """Dream family adapter (Dream-v0-Instruct-7B).
2
+
3
+ Load path mirrors the proven npc-engine harness: BitsAndBytesConfig for
4
+ 8-bit (never a raw load_in_8bit kwarg) and the recipe's transformers 4.x
5
+ pin (5.x breaks Dream's RoPE config with KeyError 'default' — proven live
6
+ on the 5060 Ti run, 2026-10-25; verified on 4.53.3 and 4.57.6).
7
+
8
+ UNVERIFIED in ollada until the Phase 1 real-load gate passes on a T4.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+
15
+ import torch
16
+ from transformers import AutoModel, AutoTokenizer
17
+
18
+ from ollada.adapters.base import BaseAdapter, GenResult, quantization_config
19
+
20
+ _DTYPES = {"float16": torch.float16, "bfloat16": torch.bfloat16}
21
+
22
+
23
+ class DreamAdapter(BaseAdapter):
24
+ family = "dream"
25
+
26
+ def load(self) -> None:
27
+ recipe = self.recipe
28
+ self.tokenizer = AutoTokenizer.from_pretrained(recipe.source, trust_remote_code=True)
29
+ kwargs = {
30
+ "trust_remote_code": True,
31
+ "torch_dtype": _DTYPES.get(recipe.dtype, torch.float16),
32
+ }
33
+ qc = quantization_config(recipe.quant_default)
34
+ if qc is not None:
35
+ kwargs["quantization_config"] = qc
36
+ kwargs["device_map"] = "auto"
37
+ self.model = AutoModel.from_pretrained(recipe.source, **kwargs)
38
+ if qc is None and torch.cuda.is_available():
39
+ self.model = self.model.cuda()
40
+ self.model.eval()
41
+
42
+ @torch.no_grad()
43
+ def chat(self, messages, overrides=None):
44
+ self._require_loaded()
45
+ params = self._params(overrides)
46
+ input_ids = self._prompt_ids(messages).to(self.model.device)
47
+ started = time.time()
48
+ out = self.model.diffusion_generate(
49
+ input_ids,
50
+ max_new_tokens=int(params.get("gen_length", 256)),
51
+ steps=int(params.get("steps", 32)),
52
+ temperature=float(params.get("temperature", 0.2)),
53
+ top_p=float(params.get("top_p", 0.95)),
54
+ alg="origin",
55
+ alg_temp=0.0,
56
+ mask_id=self.model.config.mask_token_id,
57
+ )
58
+ ids = out.sequences if hasattr(out, "sequences") else out
59
+ return self._finish(ids, input_ids.shape[1], started)
@@ -0,0 +1,70 @@
1
+ """Nemotron-Labs Diffusion family adapter (NVIDIA).
2
+
3
+ One checkpoint, three modes — the recipe picks the default:
4
+ diffusion -> model.generate(ids, block_length, threshold, eos_token_id)
5
+ ar -> model.ar_generate(ids)
6
+ linear_spec -> model.linear_spec_generate(ids, block_length, eos_token_id)
7
+ Each returns (out_ids, nfe). Requires transformers>=5.0.0 (recipe pin) and
8
+ therefore runs in the isolated nemotron worker env (PLAN.md section 3).
9
+
10
+ UNVERIFIED in ollada until a real-load gate passes on a T4.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import time
16
+
17
+ import torch
18
+ from transformers import AutoModel, AutoTokenizer
19
+
20
+ from ollada.adapters.base import BaseAdapter, quantization_config
21
+
22
+ _DTYPES = {"float16": torch.float16, "bfloat16": torch.bfloat16}
23
+
24
+
25
+ class NemotronAdapter(BaseAdapter):
26
+ family = "nemotron"
27
+
28
+ def load(self) -> None:
29
+ recipe = self.recipe
30
+ self.tokenizer = AutoTokenizer.from_pretrained(recipe.source, trust_remote_code=True)
31
+ kwargs = {"trust_remote_code": True}
32
+ qc = quantization_config(recipe.quant_default)
33
+ if qc is not None:
34
+ kwargs["quantization_config"] = qc
35
+ kwargs["device_map"] = "auto"
36
+ model = AutoModel.from_pretrained(recipe.source, **kwargs)
37
+ if qc is None:
38
+ dtype = _DTYPES.get(recipe.dtype, torch.bfloat16)
39
+ model = model.to(dtype)
40
+ if torch.cuda.is_available():
41
+ model = model.cuda()
42
+ self.model = model.eval()
43
+
44
+ @torch.no_grad()
45
+ def chat(self, messages, overrides=None):
46
+ self._require_loaded()
47
+ params = self._params(overrides)
48
+ input_ids = self._prompt_ids(messages).to(self.model.device)
49
+ mode = str(params.get("mode", "diffusion"))
50
+ max_new = int(params.get("max_new_tokens", 512))
51
+ eos = self.tokenizer.eos_token_id
52
+ started = time.time()
53
+ if mode == "ar":
54
+ out_ids, nfe = self.model.ar_generate(input_ids, max_new_tokens=max_new)
55
+ elif mode == "linear_spec":
56
+ out_ids, nfe = self.model.linear_spec_generate(
57
+ input_ids,
58
+ max_new_tokens=max_new,
59
+ block_length=int(params.get("block_length", 32)),
60
+ eos_token_id=eos,
61
+ )
62
+ else:
63
+ out_ids, nfe = self.model.generate(
64
+ input_ids,
65
+ max_new_tokens=max_new,
66
+ block_length=int(params.get("block_length", 32)),
67
+ threshold=float(params.get("threshold", 0.9)),
68
+ eos_token_id=eos,
69
+ )
70
+ return self._finish(out_ids, input_ids.shape[1], started, nfe=int(nfe))