Download tests/test_caption_processing_for_cache.py from trixlyaisolutions/onnx: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/trixlyaisolutions/onnx/resolve/main/tests/test_caption_processing_for_cache.py
- Command line
-
hf download hf://trixlyaisolutions/onnx/tests/test_caption_processing_for_cache.py
-
curl -L -o test_caption_processing_for_cache.py https://huggingface.co/trixlyaisolutions/onnx/resolve/main/tests/test_caption_processing_for_cache.py
15.4 kB
| """Caption processing when caching Text Encoder outputs (issue #2453). | |
| The deterministic part of ``process_caption`` (prefix/suffix, secondary separator, replacements, first-line | |
| truncation) must be applied to the captions passed to ``cache_batch_outputs``. | |
| """ | |
| import logging | |
| import random | |
| from types import SimpleNamespace | |
| from unittest.mock import patch | |
| import numpy as np | |
| import pytest | |
| import torch | |
| from library.dataset import BaseDataset, BucketBatchIndex, ImageInfo | |
| from library.strategy_base import TextEncoderOutputsCachingStrategy, TextEncodingStrategy, TokenizeStrategy | |
| from library.subset import DEFAULT_MULTILINE_CAPTION_SEPARATOR | |
| SEP = DEFAULT_MULTILINE_CAPTION_SEPARATOR | |
| def make_subset(**overrides): | |
| params = dict( | |
| enable_multiline_caption=False, | |
| multiline_caption_separator=SEP, | |
| enable_wildcard=False, | |
| caption_prefix=None, | |
| caption_suffix=None, | |
| caption_dropout_rate=0.0, | |
| caption_dropout_every_n_epochs=0, | |
| caption_tag_dropout_rate=0.0, | |
| shuffle_caption=False, | |
| token_warmup_min=1, | |
| token_warmup_step=0, | |
| keep_tokens=0, | |
| keep_tokens_separator="", | |
| caption_separator=",", | |
| secondary_separator=None, | |
| ) | |
| params.update(overrides) | |
| return SimpleNamespace(**params) | |
| def make_dataset(): | |
| ds = SimpleNamespace( | |
| current_epoch=0, current_step=0, max_train_steps=100, replacements={}, has_multiple_caption_candidates=False | |
| ) | |
| # process_caption_for_cache delegates to this method | |
| ds.process_caption_candidates_for_cache = lambda subset, caption: BaseDataset.process_caption_candidates_for_cache( | |
| ds, subset, caption | |
| ) | |
| return ds | |
| def for_cache(subset, caption, replacements=None): | |
| ds = make_dataset() | |
| if replacements: | |
| ds.replacements = replacements | |
| return BaseDataset.process_caption_for_cache(ds, subset, caption) | |
| # --------------------------------------------------------------------------- | |
| # process_caption_for_cache | |
| # --------------------------------------------------------------------------- | |
| def test_for_cache_applies_prefix_suffix_and_first_line(): | |
| subset = make_subset(caption_prefix="pre", caption_suffix="suf") | |
| assert for_cache(subset, "line1\nline2") == "pre line1 suf" | |
| def test_for_cache_applies_secondary_separator_and_replacements(): | |
| subset = make_subset(secondary_separator=";;;") | |
| assert for_cache(subset, "a;;;b, girl", replacements={"girl": "woman"}) == "a,b, woman" | |
| def test_for_cache_is_deterministic_and_ignores_random_options(): | |
| # dropout / shuffle / tag dropout are not applied (they are rejected by is_text_encoder_output_cacheable anyway) | |
| subset = make_subset(caption_dropout_rate=1.0, shuffle_caption=True, caption_tag_dropout_rate=1.0, caption_prefix="pre") | |
| assert for_cache(subset, "a, b, c") == "pre a, b, c" | |
| def test_for_cache_multiline_single_candidate_is_normalized(): | |
| subset = make_subset(enable_multiline_caption=True, caption_suffix="suf") | |
| assert for_cache(subset, " a \nb\n\n") == " a\nb suf" | |
| def test_for_cache_wildcard_caption_is_kept_as_is(): | |
| subset = make_subset(enable_wildcard=True, caption_prefix="pre") | |
| assert for_cache(subset, "a {x|y}\nb") == "pre a {x|y}\nb" | |
| def test_for_cache_matches_process_caption_for_plain_captions(): | |
| subset = make_subset(caption_prefix="pre", caption_suffix="suf", secondary_separator=";;;") | |
| ds = make_dataset() | |
| caption = "a;;;b, c" | |
| assert BaseDataset.process_caption_for_cache(ds, subset, caption) == BaseDataset.process_caption(ds, subset, caption) | |
| # --------------------------------------------------------------------------- | |
| # is_text_encoder_output_cacheable: wildcard warning | |
| # --------------------------------------------------------------------------- | |
| def test_wildcard_warns_but_is_cacheable(caplog): | |
| ds = make_dataset() | |
| ds.subsets = [make_subset(enable_wildcard=True)] | |
| with caplog.at_level(logging.WARNING, logger="library.dataset"): | |
| assert BaseDataset.is_text_encoder_output_cacheable(ds) is True | |
| assert any("enable_wildcard" in r.getMessage() for r in caplog.records) | |
| def test_no_wildcard_no_warning(caplog): | |
| ds = make_dataset() | |
| ds.subsets = [make_subset()] | |
| with caplog.at_level(logging.WARNING, logger="library.dataset"): | |
| assert BaseDataset.is_text_encoder_output_cacheable(ds) is True | |
| assert not any("enable_wildcard" in r.getMessage() for r in caplog.records) | |
| # --------------------------------------------------------------------------- | |
| # new_cache_text_encoder_outputs passes processed captions to the caching strategy | |
| # --------------------------------------------------------------------------- | |
| class RecordingCachingStrategy(TextEncoderOutputsCachingStrategy): | |
| def __init__(self, batch_size): | |
| super().__init__(cache_to_disk=False, batch_size=batch_size, skip_disk_cache_validity_check=False) | |
| self.calls = [] | |
| def cache_batch_outputs(self, tokenize_strategy, models, text_encoding_strategy, batch, captions): | |
| self.calls.append(([info.image_key for info in batch], list(captions))) | |
| def test_new_cache_text_encoder_outputs_uses_processed_captions(): | |
| ds = BaseDataset((64, 64), 1.0, False, False) | |
| ds.batch_size = 2 | |
| subset_a = make_subset(caption_prefix="pre", secondary_separator=";;;") | |
| subset_b = make_subset(enable_multiline_caption=True, caption_suffix="suf") | |
| ds.subsets = [subset_a, subset_b] | |
| infos = [ | |
| (ImageInfo("img0", 1, "a;;;b\nignored", False, "img0.png"), subset_a), | |
| (ImageInfo("img1", 1, "c", False, "img1.png"), subset_a), | |
| (ImageInfo("img2", 1, "x\ny\n", False, "img2.png"), subset_b), | |
| ] | |
| for info, subset in infos: | |
| ds.register_image(info, subset) | |
| caching = RecordingCachingStrategy(batch_size=2) | |
| accelerator = SimpleNamespace(num_processes=1, process_index=0) | |
| with patch.object(TokenizeStrategy, "get_strategy", return_value=object()), patch.object( | |
| TextEncodingStrategy, "get_strategy", return_value=object() | |
| ), patch.object(TextEncoderOutputsCachingStrategy, "get_strategy", return_value=caching): | |
| BaseDataset.new_cache_text_encoder_outputs(ds, models=[], accelerator=accelerator) | |
| assert caching.calls == [ | |
| (["img0", "img1"], ["pre a,b", "pre c"]), | |
| (["img2"], ["x\ny suf"]), | |
| ] | |
| # the raw captions are untouched | |
| assert [info.caption for info, _ in infos] == ["a;;;b\nignored", "c", "x\ny\n"] | |
| # --------------------------------------------------------------------------- | |
| # multiple caption candidates: process_caption_candidates_for_cache | |
| # --------------------------------------------------------------------------- | |
| def candidates_for_cache(subset, caption): | |
| return BaseDataset.process_caption_candidates_for_cache(make_dataset(), subset, caption) | |
| def test_candidates_for_cache_multiline_returns_all_candidates_with_prefix_suffix(): | |
| subset = make_subset(enable_multiline_caption=True, caption_prefix="pre", caption_suffix="suf") | |
| caption = f"a\nb\n{SEP} # comment\nc\n{SEP}\n\n" | |
| assert candidates_for_cache(subset, caption) == ["pre a\nb suf", "pre c suf"] | |
| def test_candidates_for_cache_single_candidate_modes(): | |
| assert candidates_for_cache(make_subset(), "a\nb") == ["a"] | |
| assert candidates_for_cache(make_subset(enable_wildcard=True), "a {x|y}\nb") == ["a {x|y}\nb"] | |
| assert candidates_for_cache(make_subset(enable_multiline_caption=True), "a\nb") == ["a\nb"] | |
| # process_caption_for_cache is the first candidate | |
| subset = make_subset(enable_multiline_caption=True) | |
| assert for_cache(subset, f"a\n{SEP}\nb") == "a" | |
| def test_cacheable_with_multiple_candidates_only_if_supported(caplog): | |
| ds = make_dataset() | |
| ds.subsets = [make_subset(enable_multiline_caption=True)] | |
| ds.has_multiple_caption_candidates = True | |
| with caplog.at_level(logging.INFO, logger="library.dataset"): | |
| assert BaseDataset.is_text_encoder_output_cacheable(ds) is False | |
| assert BaseDataset.is_text_encoder_output_cacheable(ds, supports_multiple_caption_candidates=True) is True | |
| assert any("multiple candidates" in r.getMessage() and r.levelno == logging.ERROR for r in caplog.records) | |
| assert any("all candidates" in r.getMessage() and r.levelno == logging.INFO for r in caplog.records) | |
| # the other restrictions still apply | |
| ds.subsets = [make_subset(enable_multiline_caption=True, shuffle_caption=True)] | |
| assert BaseDataset.is_text_encoder_output_cacheable(ds, supports_multiple_caption_candidates=True) is False | |
| # --------------------------------------------------------------------------- | |
| # new_cache_text_encoder_outputs with multiple candidates | |
| # --------------------------------------------------------------------------- | |
| class RecordingMultiCachingStrategy(TextEncoderOutputsCachingStrategy): | |
| def __init__(self, batch_size, cache_to_disk=False, valid_npz=()): | |
| super().__init__(cache_to_disk=cache_to_disk, batch_size=batch_size, skip_disk_cache_validity_check=False) | |
| self.calls = [] | |
| self.checked = [] | |
| self.valid_npz = set(valid_npz) | |
| def supports_multiple_caption_candidates(self): | |
| return True | |
| def get_outputs_npz_path(self, image_abs_path): | |
| return image_abs_path + "_te.npz" | |
| def is_disk_cached_outputs_expected(self, npz_path, captions=None): | |
| self.checked.append((npz_path, captions)) | |
| return npz_path in self.valid_npz | |
| def cache_batch_outputs(self, tokenize_strategy, models, text_encoding_strategy, batch, captions): | |
| raise AssertionError("cache_batch_outputs must not be called for a strategy with multiple candidate support") | |
| def cache_batch_outputs_multi(self, tokenize_strategy, models, text_encoding_strategy, batch, captions_list): | |
| self.calls.append(([info.image_key for info in batch], [list(c) for c in captions_list])) | |
| def make_multi_candidate_dataset(): | |
| ds = BaseDataset((64, 64), 1.0, False, False) | |
| ds.batch_size = 3 | |
| subset = make_subset(enable_multiline_caption=True, caption_prefix="pre") | |
| ds.subsets = [subset] | |
| infos = [ | |
| ImageInfo("img0", 1, f"a\n{SEP}\nb\n{SEP}\nc", False, "img0.png"), # 3 candidates | |
| ImageInfo("img1", 1, "d", False, "img1.png"), # 1 candidate | |
| ImageInfo("img2", 1, f"e\n{SEP}\nf", False, "img2.png"), # 2 candidates | |
| ImageInfo("img3", 1, "g", False, "img3.png"), # 1 candidate | |
| ] | |
| for info in infos: | |
| ds.register_image(info, subset) | |
| ds.has_multiple_caption_candidates = True | |
| return ds, infos | |
| def run_cache(ds, caching): | |
| accelerator = SimpleNamespace(num_processes=1, process_index=0) | |
| with patch.object(TokenizeStrategy, "get_strategy", return_value=object()), patch.object( | |
| TextEncodingStrategy, "get_strategy", return_value=object() | |
| ), patch.object(TextEncoderOutputsCachingStrategy, "get_strategy", return_value=caching): | |
| BaseDataset.new_cache_text_encoder_outputs(ds, models=[], accelerator=accelerator) | |
| def test_new_cache_text_encoder_outputs_batches_by_number_of_candidates(): | |
| ds, _ = make_multi_candidate_dataset() | |
| caching = RecordingMultiCachingStrategy(batch_size=3) | |
| run_cache(ds, caching) | |
| # the batch is flushed when the number of candidates reaches batch_size (3): img0 alone, img1+img2, img3 | |
| assert caching.calls == [ | |
| (["img0"], [["pre a", "pre b", "pre c"]]), | |
| (["img1", "img2"], [["pre d"], ["pre e", "pre f"]]), | |
| (["img3"], [["pre g"]]), | |
| ] | |
| def test_new_cache_text_encoder_outputs_checks_disk_cache_with_candidates(): | |
| ds, infos = make_multi_candidate_dataset() | |
| caching = RecordingMultiCachingStrategy(batch_size=10, cache_to_disk=True, valid_npz={"img1.png_te.npz"}) | |
| run_cache(ds, caching) | |
| assert caching.checked == [ | |
| ("img0.png_te.npz", ["pre a", "pre b", "pre c"]), | |
| ("img1.png_te.npz", ["pre d"]), | |
| ("img2.png_te.npz", ["pre e", "pre f"]), | |
| ("img3.png_te.npz", ["pre g"]), | |
| ] | |
| assert caching.calls == [(["img0", "img2", "img3"], [["pre a", "pre b", "pre c"], ["pre e", "pre f"], ["pre g"]])] | |
| assert [info.text_encoder_outputs_npz for info in infos] == [f"img{i}.png_te.npz" for i in range(4)] | |
| def test_new_cache_text_encoder_outputs_rejects_multiple_candidates_without_support(): | |
| ds, _ = make_multi_candidate_dataset() | |
| caching = RecordingCachingStrategy(batch_size=3) # no multiple candidate support | |
| with pytest.raises(ValueError, match="multiple candidates"): | |
| run_cache(ds, caching) | |
| assert caching.calls == [] | |
| # --------------------------------------------------------------------------- | |
| # __getitem__ chooses one candidate and returns the matching caption | |
| # --------------------------------------------------------------------------- | |
| class IndexRecordingStrategy(RecordingMultiCachingStrategy): | |
| def __init__(self): | |
| super().__init__(batch_size=1, cache_to_disk=True) | |
| self.loaded = [] | |
| def load_outputs_npz(self, npz_path, candidate_index=0): | |
| self.loaded.append((npz_path, candidate_index)) | |
| # outputs of candidate k: embeds filled with k, mask of ones, rate | |
| return [ | |
| np.full((2, 4), float(candidate_index), dtype=np.float32), | |
| np.ones(2, dtype=np.int32), | |
| np.array(0.0, dtype=np.float32), | |
| ] | |
| def make_getitem_dataset(in_memory: bool): | |
| ds = BaseDataset((64, 64), 1.0, False, False) | |
| ds.batch_size = 2 | |
| ds.prior_loss_weight = 1.0 | |
| subset = make_subset(enable_multiline_caption=True, caption_suffix="suf") | |
| subset.flip_aug = False | |
| subset.custom_attributes = {} | |
| ds.subsets = [subset] | |
| infos = [ | |
| ImageInfo("img0", 1, f"a\n{SEP}\nb\n{SEP}\nc", False, "img0.png"), | |
| ImageInfo("img1", 1, "d", False, "img1.png"), | |
| ] | |
| strategy = IndexRecordingStrategy() | |
| for info in infos: | |
| ds.register_image(info, subset) | |
| info.latents = torch.zeros(4, 8, 8) | |
| info.latents_original_size = (64, 64) | |
| info.latents_crop_ltrb = (0, 0, 0, 0) | |
| info.alpha_mask = None | |
| info.bucket_reso = (64, 64) | |
| if in_memory: | |
| num = len(BaseDataset.process_caption_candidates_for_cache(ds, subset, info.caption)) | |
| info.text_encoder_outputs = [strategy.load_outputs_npz("mem", k) for k in range(num)] | |
| else: | |
| info.text_encoder_outputs_npz = strategy.get_outputs_npz_path(info.absolute_path) | |
| strategy.loaded.clear() | |
| ds.text_encoder_output_caching_strategy = strategy | |
| ds.bucket_manager = SimpleNamespace(buckets=[["img0", "img1"]]) | |
| ds.buckets_indices = [BucketBatchIndex(0, 2, 0)] | |
| return ds, strategy | |
| def test_getitem_chooses_candidate_and_matching_caption(in_memory): | |
| ds, strategy = make_getitem_dataset(in_memory) | |
| chosen = set() | |
| for seed in range(20): | |
| random.seed(seed) | |
| example = ds[0] | |
| embeds = example["text_encoder_outputs_list"][0] # [B, L, C] | |
| k0 = int(embeds[0, 0, 0].item()) | |
| chosen.add(k0) | |
| assert example["captions"] == [["a suf", "b suf", "c suf"][k0], "d suf"] | |
| assert int(embeds[1, 0, 0].item()) == 0 | |
| assert example["input_ids_list"] is None | |
| if not in_memory: | |
| assert strategy.loaded[-2:] == [("img0.png_te.npz", k0), ("img1.png_te.npz", 0)] | |
| assert chosen == {0, 1, 2} | |