Spaces:
Sleeping
Sleeping
File size: 6,904 Bytes
34f3bc9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """Tokenize free-text annotation columns (e.g. `annotation.substask`) into
fixed-length token tensors keyed by `annotation_tokens__<field>` /
`annotation_mask__<field>`.
The model-side CE loss branch consumes these keys; see
`modeling_labvla._compute_annotation_ce`.
Placed late in the transform chain but BEFORE the final
`UnifyLabVLAInputsTransformFn` so that:
- the adapter has already put the raw annotation string into the sample dict
- the downstream Unify step can pass `annotation_tokens__*` through to the
collated batch
This transform is a no-op when `annotation_specs` is empty, so datasets
without annotation_losses incur zero cost.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field, replace
from typing import Any
import torch
from src.schema.annotation_loss import AnnotationLossSpec
from src.transforms.core import DataDict, DataTransformFn
@DataTransformFn.register_subclass("annotation_tokenize")
@dataclass
class AnnotationTokenizeTransformFn(DataTransformFn):
"""Tokenize each configured annotation field with the VLM tokenizer.
Args:
tokenizer_path:
HF model id or local path used to instantiate a fresh tokenizer.
Should match the VLM tokenizer used by
`Qwen3_VLProcessorTransformFn` to ensure token-id consistency.
annotation_specs:
Tuple of AnnotationLossSpec; one tokenization pass per entry.
"""
tokenizer_path: str = "Qwen/Qwen3-VL-4B-Instruct"
# Populated by hydrate_all from schema.annotation_losses. Default empty
# tuple → this transform is a cheap passthrough (no-op).
annotation_specs: tuple[AnnotationLossSpec, ...] = ()
# When true, emit tensors at the effective per-sample cap instead of the
# schema union cap. The LabVLA collator pads variable 1D token tensors back
# to the batch-local max, preserving collatability.
dynamic_shape: bool = False
# Instantiated lazily in __post_init__; not a dataclass init arg.
tokenizer: Any = field(default=None, init=False, repr=False)
def __post_init__(self) -> None:
if not self.annotation_specs:
# No-op mode: skip tokenizer load entirely. Keeps OXE-only runs
# (no annotations) free of unused HF downloads / CPU cost.
self.tokenizer = None
return
from transformers import AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(self.tokenizer_path)
def __call__(self, data: DataDict) -> DataDict:
if not self.annotation_specs or self.tokenizer is None:
return data
for spec in self.annotation_specs:
raw = data.get(spec.field, "")
if not isinstance(raw, str):
# Some annotations are stored as bytes or json strings;
# coerce to plain text. Non-coercible → empty string (fully
# masked below).
try:
raw = str(raw) if raw is not None else ""
except Exception:
raw = ""
# Per-sample max_length override. Adapters that
# know a tighter cap for the current sample (e.g. RoboInter VQA's
# task-family budgets: understanding=8, planning=64) can attach
# `f"{spec.field}_max_length"` to the sample dict. We always clamp
# against the schema-level `spec.max_length` (the schema value is
# the union upper bound; per-sample can only TIGHTEN it).
#
# Tensor SHAPE is preserved at `(spec.max_length,)` so the default
# collator can stack heterogeneous task-family samples into one
# batch. The cap manifests as: real tokens are truncated to
# `effective_max_length`; mask positions in
# `[effective_max_length, spec.max_length)` are forced to 0; ID
# positions in that range are zero-filled (so even if the LM ever
# ran on them, CE would be ignored via mask).
per_sample_key = f"{spec.field}_max_length"
per_sample_cap = data.get(per_sample_key, None)
if isinstance(per_sample_cap, int) and per_sample_cap > 0:
effective_max_length = min(per_sample_cap, spec.max_length)
else:
effective_max_length = spec.max_length
tensor_max_length = effective_max_length if self.dynamic_shape else spec.max_length
enc = self.tokenizer(
raw if raw else "",
max_length=tensor_max_length,
padding="max_length",
truncation=True,
return_tensors="pt", # direct tensor, skip list round-trip
add_special_tokens=False, # never pollute annotation with BOS/EOS
)
# return_tensors="pt" yields shape (1, L); squeeze to (L,).
ids = enc["input_ids"][0].long()
mask = enc["attention_mask"][0].bool()
# Samples with empty annotation string still get non-zero length
# (BOS/EOS depending on tokenizer) but attention_mask zeros out
# every position — guarantees zero CE contribution.
if not raw:
mask = torch.zeros_like(mask)
# Apply per-sample cap by zeroing IDs and mask beyond
# `effective_max_length`. Done in-place on the freshly-allocated
# squeezed tensors (no aliasing concern). When per-sample cap ==
# spec.max_length (the default no-override path), this is a no-op.
if not self.dynamic_shape and effective_max_length < spec.max_length:
ids[effective_max_length:] = 0
mask[effective_max_length:] = False
data[spec.batch_key_tokens()] = ids
data[spec.batch_key_mask()] = mask
# Per-field weight carried inside the batch so the model can
# compose the loss without a cross-cutting schema reference. All
# samples from the same dataset carry the same scalar — this is
# a per-sample-shape (not per-value) carrier for a schema-level
# constant.
data[spec.batch_key_weight()] = torch.tensor(
float(spec.weight), dtype=torch.float32
)
return data
def hydrate(self, ctx) -> "AnnotationTokenizeTransformFn":
# Inject this dataset's annotation specs. Empty tuple → no-op path,
# zero tokenizer load, zero overhead for OXE-style datasets.
t = replace(self, annotation_specs=tuple(ctx.schema.annotation_losses))
logging.info(
f"Hydrated {t.__class__.__name__} with "
f"{len(t.annotation_specs)} annotation_specs "
f"dynamic_shape={getattr(t, 'dynamic_shape', False)} "
f"({ctx.schema.schema_id})"
)
return t
|