Skip to content

GradientTrainingDataset

Modality-agnostic dataset for GRADIEND gradient creation and batching.

Wraps a training dataset that provides items with 'factual' and 'alternative' inputs. Batches items, optionally pads variable-length tensors (via get_padding_value), runs gradient_creator on factual/alternative, and returns source/target tensors (factual, alternative, or diff). When cache_dir and use_cached_gradients are set, cache_key_fields must be provided: list of batch dict keys whose values are included in the cache hash (e.g. ['input_text', 'label']). All listed keys must be present in every batch when caching is used.

Parameters:

Name Type Description Default
training_data Any

Dataset with len and getitem returning dicts containing at least 'factual' and 'alternative' (modality-specific, e.g. tokenizer outputs).

required
gradient_creator Any

Callable(inputs) -> gradients tensor (e.g. model.forward_pass_create_gradients).

required
source str

'factual' | 'alternative' | 'diff' | None. When None (e.g. supervised_decoder), source gradients are not computed.

'factual'
target str

Same options or None. When None (e.g. supervised_encoder), target gradients are not computed.

'diff'
cache_dir Optional[str]

Optional directory for caching gradients.

None
use_cached_gradients bool

If True and cache_dir set, load/save gradients.

True
cache_key_fields Optional[List[str]]

When caching is used, list of batch keys to include in cache hash. Required when cache_dir is set and use_cached_gradients is True. All keys must exist in batch.

None
dtype dtype

Tensor dtype.

float32
device Optional[device]

Device for tensors.

None
return_metadata bool

If True, pass through batch 'metadata'.

False
get_padding_value Optional[Callable[[str], int]]

Callable(subkey: str) -> int used when batching variable-length tensors (pad_sequence). Default 0. Text uses tokenizer.pad_token_id for 'input_ids'.

None
Source code in gradiend/trainer/core/dataset.py
def __init__(
    self,
    training_data: Any,
    gradient_creator: Any,
    *,
    source: str = 'factual',
    target: str = 'diff',
    cache_dir: Optional[str] = None,
    use_cached_gradients: bool = True,
    cache_key_fields: Optional[List[str]] = None,
    dtype: torch.dtype = torch.float32,
    device: Optional[torch.device] = None,
    return_metadata: bool = False,
    get_padding_value: Optional[Callable[[str], int]] = None,
    timing_steps: int = 0,
    timing_label: str = "gradient",
):
    assert source in source_target_keywords, f'Invalid source {source}, must be one of {source_target_keywords}'
    assert target in source_target_keywords, f'Invalid target {target}, must be one of {source_target_keywords}'

    if cache_dir is not None and use_cached_gradients and (not cache_key_fields or len(cache_key_fields) == 0):
        raise ValueError(
            "When cache_dir is set and use_cached_gradients is True, cache_key_fields must be provided "
            "(list of batch keys to include in cache hash, e.g. ['input_text', 'label'])."
        )

    device = device or torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    self.training_data = training_data
    self.batch_size = getattr(training_data, 'batch_size', None) or 1
    self.gradient_creator = gradient_creator
    self.source = source
    self.target = target
    self.cache_dir = cache_dir
    if self.cache_dir is not None:
        os.makedirs(self.cache_dir, exist_ok=True)
    self.use_cached_gradients = use_cached_gradients
    self.cache_key_fields = cache_key_fields or []
    self.dtype = dtype
    self.device = device
    self.return_metadata = return_metadata
    self._get_padding_value = get_padding_value if callable(get_padding_value) else (lambda _: 0)
    self.timing_steps = int(timing_steps or 0)
    self.timing_label = timing_label

_get_padding_value instance-attribute

_get_padding_value = get_padding_value if callable(get_padding_value) else (lambda _: 0)

batch_size instance-attribute

batch_size = getattr(training_data, 'batch_size', None) or 1

cache_dir instance-attribute

cache_dir = cache_dir

cache_key_fields instance-attribute

cache_key_fields = cache_key_fields or []

device instance-attribute

device = device

dtype instance-attribute

dtype = dtype

gradient_creator instance-attribute

gradient_creator = gradient_creator

return_metadata instance-attribute

return_metadata = return_metadata

source instance-attribute

source = source

target instance-attribute

target = target

timing_label instance-attribute

timing_label = timing_label

timing_steps instance-attribute

timing_steps = int(timing_steps or 0)

training_data instance-attribute

training_data = training_data

use_cached_gradients instance-attribute

use_cached_gradients = use_cached_gradients

__getitem__

__getitem__(index)
Source code in gradiend/trainer/core/dataset.py
def __getitem__(self, index: int) -> dict:
    timing_enabled = self.timing_steps > 0 and (index == 0 or (index + 1) % self.timing_steps == 0)
    if timing_enabled:
        self._sync_cuda_for_timing()
    t0 = time.perf_counter() if timing_enabled else 0.0
    t_merge = t0
    t_factual = t0
    t_alternative = t0
    t_combine = t0

    indices = list(range(index * self.batch_size, min((index + 1) * self.batch_size, len(self.training_data))))

    with self._exclusive_gradient_access():
        batch = self._merge_batch(indices)
        if timing_enabled:
            self._sync_cuda_for_timing()
            t_merge = time.perf_counter()

        cache_file_factual = ''
        cache_file_alternative = ''
        if self.use_cached_gradients and self.cache_dir is not None and self.cache_key_fields:
            missing = [k for k in self.cache_key_fields if k not in batch]
            if missing:
                raise KeyError(
                    f"Cache key requires batch keys {self.cache_key_fields}; missing in batch: {missing}. "
                    "Ensure training_data yields these keys when caching is used."
                )
            h = hash_it([batch[k] for k in self.cache_key_fields] + [self.dtype])
            cache_file_factual = os.path.join(self.cache_dir, f'factual_{h}.pt')
            cache_file_alternative = os.path.join(self.cache_dir, f'alternative_{h}.pt')

        factual_gradients = None
        alternative_gradients = None

        if self.use_cached_gradients and self.cache_dir is not None and cache_file_factual:
            if os.path.exists(cache_file_factual):
                factual_gradients = torch.load(cache_file_factual, weights_only=True)
            if os.path.exists(cache_file_alternative):
                alternative_gradients = torch.load(cache_file_alternative, weights_only=True)

        requires_factual = self.source in factual_computation_required_keywords or self.target in factual_computation_required_keywords
        if factual_gradients is None and requires_factual:
            factual_inputs = batch["factual"]
            factual_gradients = self.gradient_creator(factual_inputs)
            del factual_inputs
            factual_gradients = factual_gradients.to(dtype=self.dtype, device=self.device)
            if self.use_cached_gradients and self.cache_dir is not None and cache_file_factual:
                os.makedirs(self.cache_dir, exist_ok=True)
                torch.save(factual_gradients, cache_file_factual)
        if timing_enabled:
            self._sync_cuda_for_timing()
            t_factual = time.perf_counter()

        requires_alternative = self.source in alternative_computation_required_keywords or self.target in alternative_computation_required_keywords
        if alternative_gradients is None and requires_alternative:
            alternative_inputs = batch['alternative']
            alternative_gradients = self.gradient_creator(alternative_inputs)
            del alternative_inputs
            alternative_gradients = alternative_gradients.to(dtype=self.dtype, device=self.device)
            if self.use_cached_gradients and self.cache_dir is not None and cache_file_alternative:
                os.makedirs(self.cache_dir, exist_ok=True)
                torch.save(alternative_gradients, cache_file_alternative)
        if timing_enabled:
            self._sync_cuda_for_timing()
            t_alternative = time.perf_counter()

        if self.source == 'factual':
            source_tensor = factual_gradients
        elif self.source == 'alternative':
            source_tensor = alternative_gradients
        elif self.source == 'diff':
            source_tensor = factual_gradients - alternative_gradients
        elif self.source is None:
            source_tensor = None  # e.g. supervised_decoder: only target needed
        else:
            raise ValueError(f'Unknown source: {self.source}')

        if self.target == 'factual':
            target_tensor = factual_gradients
        elif self.target == 'alternative':
            target_tensor = alternative_gradients
        elif self.target == 'diff':
            target_tensor = source_tensor.clone() if self.source == 'diff' else (factual_gradients - alternative_gradients)
        elif self.target is None:
            target_tensor = None
        else:
            raise ValueError(f'Unknown target: {self.target}')

        del factual_gradients
        del alternative_gradients

        output = {'source': source_tensor, 'target': target_tensor}
        for key in batch:
            if key not in output and key not in {'metadata', 'factual', 'alternative'}:
                output[key] = batch[key]
        # Label metadata must describe the gradient exposed as source.
        # For binary pairs, the alternative side is the opposite feature class.
        if self.source == 'alternative' and 'label' in output:
            output['label'] = _invert_numeric_label(output['label'])
        if self.return_metadata and 'metadata' in batch:
            output['metadata'] = batch['metadata']
    if timing_enabled:
        self._sync_cuda_for_timing()
        t_combine = time.perf_counter()
        logger.info(
            "%s row %s timing: merge=%.3fs, factual=%.3fs, alternative=%.3fs, combine=%.3fs, total=%.3fs",
            self.timing_label,
            index + 1,
            t_merge - t0,
            t_factual - t_merge,
            t_alternative - t_factual,
            t_combine - t_alternative,
            t_combine - t0,
        )
    return output

__iter__

__iter__()
Source code in gradiend/trainer/core/dataset.py
def __iter__(self):
    for i in range(len(self)):
        yield self[i]

__len__

__len__()
Source code in gradiend/trainer/core/dataset.py
def __len__(self) -> int:
    return len(self.training_data) // self.batch_size

_exclusive_gradient_access

_exclusive_gradient_access()

Serialize tokenization and base forward/backward for one gradient row.

Source code in gradiend/trainer/core/dataset.py
def _exclusive_gradient_access(self):
    """Serialize tokenization and base forward/backward for one gradient row."""
    creator = self.gradient_creator
    model = getattr(creator, "__self__", None)
    if model is not None and hasattr(model, "exclusive_base_gradient_access"):
        return model.exclusive_base_gradient_access()
    return nullcontext()

_merge_batch

_merge_batch(indices)

Collect items at indices and merge into one batch; pad variable-length tensors when needed.

Source code in gradiend/trainer/core/dataset.py
def _merge_batch(self, indices: list) -> dict:
    """Collect items at indices and merge into one batch; pad variable-length tensors when needed."""
    if len(indices) == 1:
        return self.training_data[indices[0]]

    batch = {}
    for idx in indices:
        data = self.training_data[idx]
        for key in data:
            if key not in batch:
                batch[key] = []
            batch[key].append(data[key])

    for key in batch:
        if not (isinstance(batch[key], list) and all(isinstance(d, dict) for d in batch[key])):
            continue
        first = batch[key][0]
        first_key = next(iter(first))
        if not hasattr(first[first_key], 'shape'):
            raise NotImplementedError(
                'Nested dictionary structure in batch without tensor shapes detected. '
                'This is an unexpected edge case. Please report this issue.'
            )
        needs_padding = any(
            any(d[subkey].shape != first[subkey].shape for d in batch[key])
            for subkey in first
        )
        if needs_padding:
            padded = {}
            for subkey in first:
                tensors = [d[subkey] for d in batch[key]]
                padding_value = self._get_padding_value(subkey)
                padded[subkey] = pad_sequence(tensors, batch_first=True, padding_value=padding_value)
            batch[key] = padded
        else:
            batch[key] = {subkey: torch.stack([d[subkey] for d in batch[key]]) for subkey in first}
    return batch

_sync_cuda_for_timing staticmethod

_sync_cuda_for_timing()
Source code in gradiend/trainer/core/dataset.py
@staticmethod
def _sync_cuda_for_timing() -> None:
    if torch.cuda.is_available():
        try:
            torch.cuda.synchronize()
        except Exception:
            pass