| """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) |
|
|
| |
| |
| 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() |
|
|