使用缓存进行文本生成的最佳实践
高效的缓存对于优化各种生成任务(包括文本生成、翻译、摘要和其他基于 Transformer 的应用)中的模型性能至关重要。有效的缓存有助于减少计算时间并提高响应速度,尤其是在实时或资源密集型应用中。
Transformers 支持各种缓存方法,利用“Cache”类来抽象和管理缓存逻辑。本文档概述了使用这些类以最大化性能和效率的最佳实践。请查看 API 文档 中所有可用的 Cache
类。
什么是缓存以及为什么我们应该关注它?
想象一下,你正在与某人交谈,而不是记住之前说过的话,你每次回应都需要从头开始。这会很慢且效率低下,对吧?在 Transformer 模型的世界中,也存在类似的概念,这就是键值缓存发挥作用的地方。从现在起,我将这个概念称为 KV 缓存。
KV 缓存是优化自回归模型中生成过程所必需的,在自回归模型中,模型逐个预测文本标记。这个过程可能很慢,因为模型一次只能生成一个标记,并且每个新的预测都依赖于之前的上下文。这意味着,要预测生成中的第 1000 个标记,你需要来自之前 999 个标记的信息,这些信息以这些标记表示形式的某些矩阵乘法形式出现。但是,要预测第 1001 个标记,你还需要来自前 999 个标记的相同信息,加上来自第 1000 个标记的其他信息。这就是键值缓存用于优化顺序生成过程的地方,它通过存储先前的计算结果以便在后续标记中重用,从而避免重复计算。
更具体地说,键值缓存充当这些生成模型的内存库,模型在其中存储从自注意力层派生的先前处理过的标记的键值对。通过存储此信息,模型可以避免冗余计算,而是从缓存中检索先前标记的键和值。请注意,缓存仅可用于推理,在训练时应禁用,否则可能会导致意外错误。
对于喜欢深入研究的好奇心
幕后:缓存对象如何在注意力机制中工作
当在输入中利用缓存对象时,注意力模块会执行几个关键步骤,以无缝地整合过去和现在的信息。
注意力模块将当前键值与存储在缓存中的过去键值连接起来。这将导致形状为 (new_tokens_length, past_kv_length + new_tokens_length)
的注意力权重。本质上,过去和当前的键值组合起来计算注意力分数,确保模型同时考虑之前的上下文和新的输入。连接的键值用于计算注意力分数,从而产生形状为 (new_tokens_length, past_kv_length + new_tokens_length)
的注意力权重。
因此,当迭代调用 forward()
而不是 generate()
方法时,必须确保注意力掩码的形状与过去和当前键值的组合长度匹配。注意力掩码的形状应为 (batch_size, past_kv_length + new_tokens_length)
。当你调用 generate()
方法时,这通常在内部处理。如果你想使用 Cache 类实现自己的生成循环,请考虑这一点并准备注意力掩码以保存当前和过去标记的值。
在编写自己的生成循环时,你需要了解的一个重要概念是 cache_position
。如果你想通过调用 forward()
来重用一个已经填充的 Cache 对象,则必须传入一个有效的 cache_position
,它将指示序列中输入的位置。请注意,cache_position
不受填充的影响,并且始终为每个标记添加一个位置。例如,如果键/值缓存包含 10 个标记(无论其中有多少个是填充标记),则下一个标记的缓存位置应为 torch.tensor([10])
。
请参见下面的示例,了解如何实现自己的生成循环。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM, DynamicCache
>>> model_id = "meta-llama/Llama-2-7b-chat-hf"
>>> model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, device_map="cuda:0")
>>> tokenizer = AutoTokenizer.from_pretrained(model_id)
>>> past_key_values = DynamicCache()
>>> messages = [{"role": "user", "content": "Hello, what's your name."}]
>>> inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt", return_dict=True).to("cuda:0")
>>> generated_ids = inputs.input_ids
>>> cache_position = torch.arange(inputs.input_ids.shape[1], dtype=torch.int64, device="cuda:0")
>>> max_new_tokens = 10
>>> for _ in range(max_new_tokens):
... outputs = model(**inputs, cache_position=cache_position, past_key_values=past_key_values, use_cache=True)
... # Greedily sample one next token
... next_token_ids = outputs.logits[:, -1:].argmax(-1)
... generated_ids = torch.cat([generated_ids, next_token_ids], dim=-1)
...
... # Prepare inputs for the next generation step by leaaving unprocessed tokens, in our case we have only one new token
... # and expanding attn mask for the new token, as explained above
... attention_mask = inputs["attention_mask"]
... attention_mask = torch.cat([attention_mask, attention_mask.new_ones((attention_mask.shape[0], 1))], dim=-1)
... inputs = {"input_ids": next_token_ids, "attention_mask": attention_mask}
... cache_position = cache_position[-1:] + 1 # add one more position for the next token
>>> print(tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0])
"[INST] Hello, what's your name. [/INST] Hello! My name is LLaMA,"
使用缓存进行生成
在 🤗 Transformers 中,我们支持各种缓存类型以优化不同模型和任务的性能。默认情况下,所有模型都使用缓存进行生成,其中 ~DynamicCache 类是大多数模型的默认缓存。它允许我们动态增加缓存大小,通过在我们生成时保存越来越多的键和值。如果由于某些原因你不想使用缓存,可以在 generate()
方法中传递 use_cache=False
。
请参考下表了解缓存类型之间的区别,并选择最适合你的用例的类型。建议初始化的模型应在调用模型之前进行初始化,并作为关键字参数传递给模型。在所有其他情况下,你只需定义所需的 cache_implementation
,我们就会为你处理剩下的事情。
缓存类型 | 内存效率 | 支持 torch.compile() | 建议初始化 | 延迟 | 长文本生成 |
---|---|---|---|---|---|
动态缓存 | 否 | 否 | 否 | 中等 | 否 |
静态缓存 | 否 | 是 | 是 | 高 | 否 |
卸载缓存 | 是 | 否 | 否 | 低 | 是 |
卸载静态缓存 | 否 | 是 | 是 | 高 | 是 |
量化缓存 | 是 | 否 | 否 | 低 | 是 |
滑动窗口缓存 | 否 | 是 | 是 | 高 | 否 |
Sink 缓存 | 是 | 否 | 是 | 中等 | 是 |
这些缓存类可以通过生成时的 cache_implementation
参数设置。要了解 cache_implementation
标志的可用选项,请参考 API 文档。现在,让我们详细探讨每种缓存类型,并了解如何使用它们。请注意,以下示例适用于仅解码器的基于 Transformer 的模型。我们还支持 Mamba 或 Jamba 等模型的 [“模型特定缓存”] 类,请继续阅读以了解更多详细信息。
量化缓存
键值缓存可能会占用大量内存,成为 长文本生成时的瓶颈,尤其是在大型语言模型中。在使用 generate()
时对缓存进行量化可以显着降低内存需求,但会以速度为代价。
transformers
中的 KV 缓存量化很大程度上受到论文 “KIVI:用于 KV 缓存的免调优非对称 2 位量化” 的启发,目前支持 ~QuantoQuantizedCache 和 ~HQQQuantizedCache 类。有关内部工作原理的更多信息,请参阅论文。
要启用键值缓存的量化,需要在 generation_config
中指示 cache_implementation="quantized"
。量化相关的参数应作为 dict
或 ~QuantizedCacheConfig 类的实例传递给 generation_config
。必须在 ~QuantizedCacheConfig 中指示要使用哪个量化后端,默认为 quanto
。
如果使用 quanto
后端,建议将缓存配置中的 axis-key/axis-value
参数设置为 0
,如果使用 HQQ
后端,则设置为 1
。对于其他配置值,除非内存不足,否则请使用默认值。在这种情况下,可以考虑减少残差长度。
如果上下文长度较短,并且有足够的GPU VRAM可用,无需缓存量化即可运行,那么缓存量化可能会对延迟产生不利影响。建议在内存效率和延迟之间寻求平衡。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM
>>> tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
>>> model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", torch_dtype=torch.float16).to("cuda:0")
>>> inputs = tokenizer("I like rock music because", return_tensors="pt").to(model.device)
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=20, cache_implementation="quantized", cache_config={"nbits": 4, "backend": "quanto"})
>>> print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
I like rock music because it's loud and energetic. It's a great way to express myself and rel
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=20)
>>> print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
I like rock music because it's loud and energetic. I like to listen to it when I'm feeling
卸载缓存
类似于KV缓存量化,~OffloadedCache策略旨在减少GPU VRAM的使用。它通过将大多数层的KV缓存移动到CPU来实现。当模型的forward()
方法迭代各层时,此策略会将当前层的缓存保留在GPU上。同时,它会异步预取下一层的缓存,并将前一层缓存发送回CPU。与KV缓存量化不同,此策略始终产生与默认KV缓存实现相同的结果。因此,它可以作为它的直接替代品或后备方案。
根据您的模型和生成任务的特征(上下文大小、生成标记的数量、光束的数量等),您可能会注意到与默认KV缓存实现相比,生成吞吐量略有下降。
要启用KV缓存卸载,请在generation_config
中或直接在generate()
调用中传递cache_implementation="offloaded"
。对于卸载静态缓存,请使用cache_implementation="offloaded_static"
(另请参见下面卸载静态缓存)。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM
>>> ckpt = "microsoft/Phi-3-mini-4k-instruct"
>>> tokenizer = AutoTokenizer.from_pretrained(ckpt)
>>> model = AutoModelForCausalLM.from_pretrained(ckpt, torch_dtype=torch.float16).to("cuda:0")
>>> inputs = tokenizer("Fun fact: The shortest", return_tensors="pt").to(model.device)
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=23, cache_implementation="offloaded")
>>> print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
Fun fact: The shortest war in history was between Britain and Zanzibar on August 27, 1896.
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=23)
>>> print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
Fun fact: The shortest war in history was between Britain and Zanzibar on August 27, 1896.
缓存卸载需要GPU,并且可能比动态KV缓存慢。如果您遇到CUDA内存不足错误,请使用它。
以下示例显示了如何将KV缓存卸载用作后备策略。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM
>>> def resilient_generate(model, *args, **kwargs):
... oom = False
... try:
... return model.generate(*args, **kwargs)
... except torch.cuda.OutOfMemoryError as e:
... print(e)
... print("retrying with cache_implementation='offloaded'")
... oom = True
... if oom:
... torch.cuda.empty_cache()
... kwargs["cache_implementation"] = "offloaded"
... return model.generate(*args, **kwargs)
...
...
>>> ckpt = "microsoft/Phi-3-mini-4k-instruct"
>>> tokenizer = AutoTokenizer.from_pretrained(ckpt)
>>> model = AutoModelForCausalLM.from_pretrained(ckpt, torch_dtype=torch.float16).to("cuda:0")
>>> prompt = ["okay "*1000 + "Fun fact: The most"]
>>> inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
>>> beams = { "num_beams": 40, "num_beam_groups": 40, "num_return_sequences": 40, "diversity_penalty": 1.0, "max_new_tokens": 23, "early_stopping": True, }
>>> out = resilient_generate(model, **inputs, **beams)
>>> responses = tokenizer.batch_decode(out[:,-28:], skip_special_tokens=True)
在具有50 GB RAM的GPU上,运行此代码将打印
CUDA out of memory. Tried to allocate 4.83 GiB. GPU
retrying with cache_implementation='offloaded'
然后成功生成40个光束。
静态缓存
由于“动态缓存”会随着每个生成步骤动态增长,因此它阻止您利用JIT优化。 ~StaticCache 预先分配了键和值的特定最大大小,允许您生成到最大长度而无需修改缓存大小。请查看下面的用法示例。
有关使用静态缓存和JIT编译的更多示例,请查看静态缓存和torchcompile
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM
>>> tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
>>> model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", torch_dtype=torch.float16, device_map="auto")
>>> inputs = tokenizer("Hello, my name is", return_tensors="pt").to(model.device)
>>> # simply pass the cache implementation="static"
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=20, cache_implementation="static")
>>> tokenizer.batch_decode(out, skip_special_tokens=True)[0]
"Hello, my name is [Your Name], and I am a [Your Profession] with [Number of Years] of"
卸载静态缓存
就像~OffloadedCache用于卸载“动态缓存”,也存在卸载静态缓存。它完全支持JIT优化。只需在generation_config
中或直接在generate()
调用中传递cache_implementation="offloaded_static"
。这将使用~OffloadedStaticCache实现。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM
>>> tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
>>> model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", torch_dtype=torch.float16, device_map="auto")
>>> inputs = tokenizer("Hello, my name is", return_tensors="pt").to(model.device)
>>> # simply pass the cache implementation="static"
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=20, cache_implementation="offloaded_static")
>>> tokenizer.batch_decode(out, skip_special_tokens=True)[0]
"Hello, my name is [Your Name], and I am a [Your Profession] with [Number of Years] of"
滑动窗口缓存
顾名思义,此缓存类型在之前的键和值上实现了一个滑动窗口,仅保留最后sliding_window
个标记。它应该与支持滑动窗口注意力的模型(如Mistral)一起使用。此外,与静态缓存类似,此缓存也适合JIT,并且可以使用与静态缓存相同的编译技术。
请注意,您只能对支持滑动窗口的模型(例如Mistral模型)使用此缓存。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM, SinkCache
>>> tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1")
>>> model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1", torch_dtype=torch.float16).to("cuda:0")
>>> inputs = tokenizer("Yesterday I was on a rock concert and.", return_tensors="pt").to(model.device)
>>> # can be used by passing in cache implementation
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=30, cache_implementation="sliding_window")
>>> tokenizer.batch_decode(out, skip_special_tokens=True)[0]
"Yesterday I was on a rock concert and. I was so excited to see my favorite band. I was so excited that I was jumping up and down and screaming. I was so excited that I"
汇聚缓存
汇聚缓存在“使用注意力汇聚的有效流式语言模型”中引入。它允许您生成长文本序列(根据论文“无限长度”),无需任何微调。这是通过智能处理之前的键和值来实现的,具体来说,它保留了序列中的一些初始标记,称为“汇聚标记”。这是基于这样的观察:在生成过程中,这些初始标记吸引了很大一部分注意力分数。在“汇聚标记”之后的标记将基于滑动窗口进行丢弃,仅保留最新的window_size
个标记。通过将这些初始标记保留为“注意力汇聚”,即使在处理非常长的文本时,模型也能保持稳定的性能,从而丢弃大部分先前知识。
与其他缓存类不同,此缓存类不能通过指示cache_implementation
直接使用。您必须在调用generate()
之前初始化缓存,如下所示。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM, SinkCache
>>> tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
>>> model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", torch_dtype=torch.float16).to("cuda:0")
>>> inputs = tokenizer("This is a long story about unicorns, fairies and magic.", return_tensors="pt").to(model.device)
>>> # get our cache, specify number of sink tokens and window size
>>> # Note that window size already includes sink tokens, so has to be larger
>>> past_key_values = SinkCache(window_length=256, num_sink_tokens=4)
>>> out = model.generate(**inputs, do_sample=False, max_new_tokens=30, past_key_values=past_key_values)
>>> tokenizer.batch_decode(out, skip_special_tokens=True)[0]
"This is a long story about unicorns, fairies and magic. It is a fantasy world where unicorns and fairies live together in harmony. The story follows a young girl named Lily"
编码器-解码器缓存
~EncoderDecoderCache 是一个旨在处理编码器-解码器模型的缓存需求的包装器。此缓存类型专门用于管理自注意力和交叉注意力缓存,确保存储和检索这些复杂模型所需的过去键/值。编码器-解码器缓存的优点是,您可以根据用例为编码器和解码器设置不同的缓存类型。目前,此缓存仅在Whisper模型中受支持,但我们很快会添加更多模型。
在使用方面,无需执行任何特殊操作,调用generate()
或forward()
将为您处理所有内容。
模型特定的缓存类
某些模型需要以特定方式存储以前的键、值或状态,并且无法使用上述缓存类。对于此类情况,我们有几个专门的缓存类,这些类专为特定模型设计。这些模型仅接受其自己的专用缓存类,不支持使用任何其他缓存类型。一些示例包括~HybridCache(适用于Gemma2系列模型)或~MambaCache(适用于Mamba架构模型)。
使用缓存的迭代生成
我们已经了解了在生成时如何使用每种缓存类型。如果您想在迭代生成设置中使用缓存,例如在聊天机器人等应用程序中,交互涉及多个轮次和持续的来回交换,该怎么办?使用缓存的迭代生成允许这些系统有效地处理正在进行的对话,而无需在每个步骤重新处理整个上下文。但在开始实施之前,您应该了解一些提示
迭代生成的一般格式如下所示。首先,您需要初始化一个您想要的类型的空缓存,然后您可以开始迭代地输入新的提示。使用聊天模板可以跟踪对话历史和格式,更多信息请参阅chat_templating
如果您使用的是 Sink Cache,则必须将输入裁剪到最大长度,因为 Sink Cache 可以生成比其最大窗口大小更长的文本,但它期望第一个输入不超过最大缓存长度。
>>> import torch
>>> from transformers import AutoTokenizer,AutoModelForCausalLM
>>> from transformers.cache_utils import (
>>> DynamicCache,
>>> SinkCache,
>>> StaticCache,
>>> SlidingWindowCache,
>>> QuantoQuantizedCache,
>>> QuantizedCacheConfig,
>>> )
>>> model_id = "meta-llama/Llama-2-7b-chat-hf"
>>> model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, device_map='auto')
>>> tokenizer = AutoTokenizer.from_pretrained(model_id)
>>> user_prompts = ["Hello, what's your name?", "Btw, yesterday I was on a rock concert."]
>>> past_key_values = DynamicCache()
>>> max_cache_length = past_key_values.get_max_length()
>>> messages = []
>>> for prompt in user_prompts:
... messages.append({"role": "user", "content": prompt})
... inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt", return_dict=True).to(model.device)
... if isinstance(past_key_values, SinkCache):
... inputs = {k: v[:, -max_cache_length:] for k, v in inputs.items()}
...
... input_length = inputs["input_ids"].shape[1]
...
... outputs = model.generate(**inputs, do_sample=False, max_new_tokens=256, past_key_values=past_key_values)
... completion = tokenizer.decode(outputs[0, input_length: ], skip_special_tokens=True)
... messages.append({"role": "assistant", "content": completion})
print(messages)
[{'role': 'user', 'content': "Hello, what's your name?"}, {'role': 'assistant', 'content': " Hello! My name is LLaMA, I'm a large language model trained by a team of researcher at Meta AI. 😊"}, {'role': 'user', 'content': 'Btw, yesterday I was on a rock concert.'}, {'role': 'assistant', 'content': ' Oh, cool! That sounds like a lot of fun! 🎉 Did you enjoy the concert? What was the band like? 🤔'}]
重复使用缓存以继续生成
有时您可能希望首先使用键/值对某些前缀提示填充缓存对象,并重复使用它多次以从中生成不同的序列。在这种情况下,您可以构造一个Cache
对象来保存指令提示,并使用不同的文本序列重复使用它几次。
>>> import copy
>>> import torch
>>> from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache, StaticCache
>>> model_id = "meta-llama/Llama-2-7b-chat-hf"
>>> model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, device_map="cuda")
>>> tokenizer = AutoTokenizer.from_pretrained(model_id)
>>> # Init StaticCache with big enough max-length (1024 tokens for the below example)
>>> # You can also init a DynamicCache, if that suits you better
>>> prompt_cache = StaticCache(config=model.config, max_batch_size=1, max_cache_len=1024, device="cuda", dtype=torch.bfloat16)
>>> INITIAL_PROMPT = "You are a helpful assistant. "
>>> inputs_initial_prompt = tokenizer(INITIAL_PROMPT, return_tensors="pt").to("cuda")
>>> # This is the common prompt cached, we need to run forward without grad to be abel to copy
>>> with torch.no_grad():
... prompt_cache = model(**inputs_initial_prompt, past_key_values = prompt_cache).past_key_values
>>> prompts = ["Help me to write a blogpost about travelling.", "What is the capital of France?"]
>>> responses = []
>>> for prompt in prompts:
... new_inputs = tokenizer(INITIAL_PROMPT + prompt, return_tensors="pt").to("cuda")
... past_key_values = copy.deepcopy(prompt_cache)
... outputs = model.generate(**new_inputs, past_key_values=past_key_values,max_new_tokens=20)
... response = tokenizer.batch_decode(outputs)[0]
... responses.append(response)
>>> print(responses)
['<s> You are a helpful assistant. Help me to write a blogpost about travelling.\n\nTitle: The Ultimate Guide to Travelling: Tips, Tricks, and', '<s> You are a helpful assistant. What is the capital of France?\n\nYes, the capital of France is Paris.</s>']
旧版缓存格式
在引入Cache
对象之前,LLM 的缓存用于表示为张量的元组的元组。旧版格式具有动态大小,在我们生成文本时会增长——非常类似于DynamicCache
。如果您的项目依赖于此旧版格式,您可以将其无缝转换为DynamicCache
并返回。
>>> import torch
>>> from transformers import AutoTokenizer, AutoModelForCausalLM, DynamicCache
>>> tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
>>> model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", torch_dtype=torch.float16, device_map="auto")
>>> inputs = tokenizer("Hello, my name is", return_tensors="pt").to(model.device)
>>> # `return_dict_in_generate=True` is required to return the cache. `return_legacy_cache` forces the returned cache
>>> # to be of the legacy type
>>> generation_outputs = model.generate(**inputs, return_dict_in_generate=True, return_legacy_cache=True, max_new_tokens=5)
>>> # We can convert a legacy cache to a DynamicCache -- and the other way around. This is helpful if you have custom
>>> # logic to manipulate a cache in a specific format.
>>> cache = DynamicCache.from_legacy_cache(generation_outputs.past_key_values)
>>> legacy_format_cache = cache.to_legacy_cache()