goldenfox's picture
Marimo Diffusion 0.6B: checkpoint, sampler, OpenAI server, ledger-needle bench
685e018 verified
Raw
History Blame Contribute Delete
12.5 kB
"""Distilled-trace preparation: reasoning traces packed with chained thought slots.
Streams a distilled-reasoning dataset (traces carrying a ``<think>...</think>`` region
followed by a final answer), segments each think region into a chain of thought units, and
renders every example through the shared slot geometry so a block-diffusion model denoises
one thought at a time. The number of thoughts varies per example, so slot count stands in
for how much the model chose to think.
Segmentation splits on paragraph breaks first, then on reasoning-marker sentence boundaries
inside long paragraphs, so a thought is a coherent reasoning move rather than a fixed-size
cut.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from pathlib import Path
from typing import Iterator
import numpy as np
from diffusion_lm.reasoning import (
ADAPTIVE_SPECIAL_TOKENS,
ExampleEncoder,
LayoutSpec,
REASONING_SPECIAL_TOKENS,
ReasoningExample,
size_token_ids,
_write_packed,
)
from diffusion_lm.tokenizer import load_tokenizer, train_tokenizer_from_iterator
_THINK_RE = re.compile(r'<think>(.*?)</think>\s*', re.DOTALL)
_MARKER_BREAK = re.compile(
r'(?<=[.!?])\s+(?=(?:Wait|Alternatively|Hmm|Okay|Now|But wait|Let me|First|Second|'
r'Next|Then|Finally|So,|Also|Another|Actually|Therefore|Thus)\b)'
)
_LONG_PARAGRAPH_CHARS = 900
def segment_thoughts(think: str) -> list[str]:
"""Split a think region into a chain of coherent thought units."""
thoughts: list[str] = []
for paragraph in re.split(r'\n\s*\n', think):
paragraph = ' '.join(paragraph.split())
if not paragraph:
continue
if len(paragraph) > _LONG_PARAGRAPH_CHARS:
thoughts.extend(part.strip() for part in _MARKER_BREAK.split(paragraph) if part.strip())
else:
thoughts.append(paragraph)
return thoughts
def parse_glaive(row: dict[str, object]) -> ReasoningExample | None:
"""Render one glaive ``reasoning-v1`` row into a chained-thought example."""
prompt = ' '.join(str(row.get('prompt') or '').split())
response = str(row.get('response') or '')
match = _THINK_RE.search(response)
if not prompt or not match:
return None
answer = ' '.join(response[match.end():].split())
thoughts = segment_thoughts(match.group(1))
if not answer or not thoughts:
return None
return ReasoningExample(prompt, tuple(thoughts), answer, expected_answer='')
PARSERS = {'glaive': parse_glaive}
def _stream_examples(
dataset: str, split: str, parser_name: str, limit: int
) -> Iterator[ReasoningExample]:
from datasets import load_dataset
parser = PARSERS[parser_name]
kept = 0
for row in load_dataset(dataset, split=split, streaming=True):
example = parser(row)
if example is None:
continue
yield example
kept += 1
if limit and kept >= limit:
return
def _is_validation(problem: str, val_fraction: float) -> bool:
digest = hashlib.sha256(problem.encode('utf-8')).digest()
return int.from_bytes(digest[:4], 'big') / 2**32 < val_fraction
def _resolve_tokenizer(
examples: list[ReasoningExample],
args: argparse.Namespace,
special_tokens: tuple[str, ...] = REASONING_SPECIAL_TOKENS,
):
from pathlib import Path
tokenizer_path = Path(args.tokenizer)
if tokenizer_path.is_file():
print(f'reusing tokenizer {tokenizer_path}')
return load_tokenizer(tokenizer_path), tokenizer_path
def _texts() -> Iterator[str]:
for example in examples:
yield f'{example.problem}\n{" ".join(example.steps)}\n{example.answer}'
tokenizer = train_tokenizer_from_iterator(
_texts(),
tokenizer_path,
vocab_size=args.vocab_size,
min_frequency=4,
length=len(examples),
extra_special_tokens=special_tokens,
)
print(f'trained tokenizer {tokenizer_path} (vocab {args.vocab_size}) on {len(examples):,} traces')
return tokenizer, tokenizer_path
def prepare(args: argparse.Namespace) -> None:
adaptive = bool(getattr(args, 'sizes', None))
spec = LayoutSpec(
seq_len=args.seq_len,
block=args.block,
max_slots=args.max_slots,
sizes=tuple(args.sizes) if adaptive else (),
)
output_dir = Path(args.output_dir)
# Buffer unique parsed examples so a native tokenizer can be trained in the same
# streaming pass the corpus is packed from, avoiding a second dataset download.
examples: list[ReasoningExample] = []
seen: set[str] = set()
scanned = 0
for example in _stream_examples(args.dataset, args.split, args.parser, args.scan_limit):
scanned += 1
key = hashlib.sha256(example.problem.encode('utf-8')).hexdigest()
if key in seen:
continue
seen.add(key)
examples.append(example)
if args.limit and len(examples) >= args.limit:
break
if not examples:
raise ValueError('no usable examples; check dataset, parser, and seq_len')
print(f'scanned {scanned:,}, buffered {len(examples):,} unique examples')
special_tokens = ADAPTIVE_SPECIAL_TOKENS if adaptive else REASONING_SPECIAL_TOKENS
tokenizer, tokenizer_path = _resolve_tokenizer(examples, args, special_tokens)
encoder = ExampleEncoder(tokenizer, spec)
if adaptive:
_pack_adaptive(examples, encoder, spec, tokenizer, tokenizer_path, output_dir, args)
return
split: dict[str, dict[str, list[np.ndarray]]] = {
'train': {'flat': [], 'flat_regions': [], 'slotted': [], 'slotted_regions': []},
'validation': {'flat': [], 'flat_regions': [], 'slotted': [], 'slotted_regions': []},
}
val_prompts: list[dict[str, str]] = []
dropped = 0
for example in examples:
encoded = encoder.encode_example(example)
if encoded is None:
dropped += 1
continue
is_val = _is_validation(example.problem, args.val_fraction)
bucket = split['validation' if is_val else 'train']
bucket['flat'].append(encoded.flat)
bucket['flat_regions'].append(encoded.flat_regions)
bucket['slotted'].append(encoded.slotted)
bucket['slotted_regions'].append(encoded.slotted_regions)
if is_val and len(val_prompts) < 500:
val_prompts.append({'problem': example.problem, 'expected_answer': ''})
total = sum(len(bucket['flat']) for bucket in split.values())
if not total:
raise ValueError('no examples survived encoding; raise seq_len or max_slots')
print(f'encoded {total:,} examples ({dropped:,} dropped at encode)')
for split_name, bucket in split.items():
if not bucket['flat']:
raise ValueError(f'no examples in the {split_name} split; lower val_fraction or scan more')
for layout in ('flat', 'slotted'):
_write_packed(
output_dir / f'{split_name}-{layout}.bin',
np.stack(bucket[layout]),
np.stack(bucket[f'{layout}_regions']),
layout=layout,
spec=spec,
tokenizer_path=tokenizer_path,
tokenizer=tokenizer,
)
print(f'{split_name}: {len(bucket["flat"]):,} examples -> {output_dir}')
problems_path = output_dir / 'validation-problems.jsonl'
with problems_path.open('w', encoding='utf-8') as handle:
for record in val_prompts:
handle.write(json.dumps(record, ensure_ascii=False) + '\n')
print(f'wrote {len(val_prompts):,} validation prompts to {problems_path}')
def _pack_adaptive(
examples: list[ReasoningExample],
encoder: ExampleEncoder,
spec: LayoutSpec,
tokenizer,
tokenizer_path: Path,
output_dir: Path,
args: argparse.Namespace,
) -> None:
"""Encode the adaptive layout and report the block-size distribution it produces."""
split: dict[str, dict[str, list[np.ndarray]]] = {
'train': {'tokens': [], 'regions': []},
'validation': {'tokens': [], 'regions': []},
}
val_prompts: list[dict[str, str]] = []
size_histogram: dict[int, int] = {size: 0 for size in spec.sizes}
blocks_per_example: list[int] = []
dropped = 0
for example in examples:
encoded = encoder.encode_adaptive(example)
if encoded is None:
dropped += 1
continue
is_val = _is_validation(example.problem, args.val_fraction)
bucket = split['validation' if is_val else 'train']
bucket['tokens'].append(encoded.tokens)
bucket['regions'].append(encoded.regions)
blocks_per_example.append(len(encoded.block_sizes))
for size in encoded.block_sizes:
size_histogram[size] += 1
if is_val and len(val_prompts) < 500:
val_prompts.append({'problem': example.problem, 'expected_answer': ''})
total = sum(len(bucket['tokens']) for bucket in split.values())
if not total:
raise ValueError('no examples survived encoding; raise seq_len, max_slots, or sizes')
total_blocks = sum(size_histogram.values())
fractions = {
size: round(count / max(1, total_blocks), 4) for size, count in size_histogram.items()
}
sorted_blocks = sorted(blocks_per_example)
def percentile(fraction: float) -> int:
return sorted_blocks[min(len(sorted_blocks) - 1, int(len(sorted_blocks) * fraction))]
print(f'encoded {total:,} examples ({dropped:,} dropped at encode)')
print(f'block-size counts {size_histogram} fractions {fractions}')
print(
f'blocks/example p50 {percentile(0.5)} p90 {percentile(0.9)} '
f'max {sorted_blocks[-1]}'
)
extra_metadata = {
'sizes': list(spec.sizes),
'size_token_ids': size_token_ids(tokenizer),
}
for split_name, bucket in split.items():
if not bucket['tokens']:
raise ValueError(f'no examples in the {split_name} split; lower val_fraction')
_write_packed(
output_dir / f'{split_name}-adaptive.bin',
np.stack(bucket['tokens']),
np.stack(bucket['regions']),
layout='adaptive',
spec=spec,
tokenizer_path=tokenizer_path,
tokenizer=tokenizer,
extra_metadata=extra_metadata,
)
print(f'{split_name}: {len(bucket["tokens"]):,} examples -> {output_dir}')
problems_path = output_dir / 'validation-problems.jsonl'
with problems_path.open('w', encoding='utf-8') as handle:
for record in val_prompts:
handle.write(json.dumps(record, ensure_ascii=False) + '\n')
print(f'wrote {len(val_prompts):,} validation prompts to {problems_path}')
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest='command', required=True)
prepare_parser = subparsers.add_parser(
'prepare', help='stream a distilled-reasoning dataset into chained-thought slots'
)
prepare_parser.add_argument('--dataset', default='glaiveai/reasoning-v1-20m')
prepare_parser.add_argument('--split', default='train')
prepare_parser.add_argument('--parser', choices=sorted(PARSERS), default='glaive')
prepare_parser.add_argument(
'--tokenizer', required=True,
help='tokenizer path; trained from the buffered traces when the file is absent'
)
prepare_parser.add_argument('--vocab-size', type=int, default=16384)
prepare_parser.add_argument('--output-dir', required=True)
prepare_parser.add_argument('--seq-len', type=int, default=2048)
prepare_parser.add_argument('--block', type=int, default=32)
prepare_parser.add_argument('--max-slots', type=int, default=64)
prepare_parser.add_argument(
'--sizes', type=int, nargs='+',
help='activate the adaptive layout with these ascending block sizes (e.g. 64 256)'
)
prepare_parser.add_argument('--val-fraction', type=float, default=0.02)
prepare_parser.add_argument(
'--limit', type=int, default=0, help='stop after this many unique kept examples'
)
prepare_parser.add_argument(
'--scan-limit', type=int, default=0, help='stop streaming after this many parsed rows'
)
return parser
def main() -> None:
args = _build_parser().parse_args()
if args.command == 'prepare':
prepare(args)
if __name__ == '__main__':
main()