梯度緩存是一種簡單的技術,用於無限地擴展對比度學習批次,遠遠超出了GPU/TPU內存約束。這意味著可以在單個GPU上進行訓練,例如8 V100 GPU,例如8 V100 GPU。此外,梯度緩存允許用戶用更具成本效益的高絨布低RAM系統替換大型RAM GPU/TPU。
該倉庫具有在我們的紙張縮放量表中描述的梯度緩存的通用實現,在內存有限的設置下進行了深層對比度學習批次大小。支持Pytorch和Jax框架。
@inproceedings{gao2021scaling,
title={Scaling Deep Contrastive Learning Batch Size under Memory Limited Setup},
author={Luyu Gao, Yunyi Zhang, Jiawei Han, Jamie Callan},
booktitle ={Proceedings of the 6th Workshop on Representation Learning for NLP},
year={2021},
}
新:我們現在支持JAX和TPU!
梯度緩存也已集成到密集的通道檢索中(DPR)。檢查我們的GC-DPR工具包。
首先安裝所需的深度學習後端,無論是Pytorch還是Jax。要安裝Gradcache,請克隆此倉庫並運行PIP。
git clone https://github.com/luyug/GradCache
cd GradCache
pip install .
為了發展,
pip install --editable .
梯度緩存功能在GradCache類中實現。如果您正在開發一個新項目,而不是修補一個舊項目,還可以結帳我們的功能方法,以減少努力的方法。
對於JAX/Flax用戶,請在此處查看一個簡單的火車功能。
該類的__init__方法定義了緩存,並具有多個功能參數*_fn ,以方便調整模型行為。另外,您也可以亞級Gradcache。
grad_cache.GradCache(
models: List[nn.Module],
chunk_sizes: Union[int, List[int]],
loss_fn: Callable[..., Tensor],
split_input_fn: Callable[[Any, int], Any] = None,
get_rep_fn: Callable[..., Tensor] = None,
fp16: bool = False,
scaler: GradScaler = None,
)
模型- 與梯度緩存一起更新的編碼模型列表。
Chunk_sizes-一個表示塊大小的整數。或每個型號的整數列表。這將控制每個模型的子批量大小以運行向前的通行證,應基於可用的GPU內存設置。一個太小的值將使GPU在使用下。
lose_fn-損耗函數,其表示數字的表示張量等於models中的模型數量和任意數字的關鍵字參數。它應該根據輸入張量計算損失,並且在任何情況下都不應修改自動射擊圖中的輸入張量的關係,後來依賴於創建梯度緩存。
split_input_fn-一個可選功能,該功能將通用模型輸入基於定義的Chunk_sizes將其拆分為塊。如果未提供,此類將盡力拆分受支持類型的輸入。請參閱split_inputs功能。
GET_REP_FN-可選功能,該功能採用通用模型輸出並返回表示張量。如果未提供,則假定通用輸出為表示張量。
FP16-如果是真的,請運行混合精度訓練,這也需要設置縮放器。
鱗片- 用於自動混合精度訓練的畢業生對象。
要運行緩存的梯度Computatoin步驟,請致電cache_step函數,
cache_step(
*model_inputs,
no_sync_except_last: bool = False,
**loss_kwargs
)
運行一個梯度緩存步驟。函數返回後,為自self.models中的每個模型計算更新,重量為梯度,就好像model_inputs是在足夠大的硬件上作為巨大的單批次運行的。使用__call__調用gradcache對像也將調用此功能。
model_inputs-每個編碼器模型的輸入列表。應該按類似於self.models順序。
no_sync_except_last-如果在分佈式設置下為true,對於每個模型,則僅觸發跨進程的跨進程的梯度減少,用於最後一個子批次的前回溯通行證。處理a)大型模型時可能會派上用場,/或b)非瑣碎數量的子批次。
lose_kwargs-到損失函數loss_fn的其他關鍵字參數。這旨在啟用靈活的損耗計算(由於Pytorch中的動態圖),例如減少,加權等。可能是,使用loss_kwargs ,您可以從高速緩存跟踪的那些編碼模型中合併輸出。
返回- 損耗,當前的步驟損失量表張量(從圖形上分離)。
model(x)傳遞model(*x)傳遞model(**x)model(*x[0], **x[1])傳遞。其他通用輸入不完全支持,我們使用以下啟發式方法執行模型調用,
model(*x)傳遞model(**x)model(*x[0], **x[1])傳遞。要與它們一起運行,應在緩存初始化期間指定split_input_fn ,以將這些輸入分解為較小的批次。在某些極少數情況下,您可能還需要覆蓋get_input_tensors ,當它的啟發式量無法捕獲足夠的張量,以覆蓋所有cuda設備,這些cuda設備在輸入中容納了一些張量。
說我們想學習標籤和文本的嵌入空間。考慮以下四對。 (實際上,您將擁有更多越來越長的文本條目。)
labels = ['fruit', 'meat', 'school', 'company']
texts = [
'this is an apple',
'steak should be cooked medium rare',
'cmu is pittsburgh',
'apple sells laptop'
]
初始化我們的編碼器模型,
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
encoder1 = AutoModel.from_pretrained("bert-base-uncased").cuda()
encoder2 = AutoModel.from_pretrained("bert-base-uncased").cuda()
初始化gradcache對象,
from grad_cache import GradCache
from grad_cache.loss import SimpleContrastiveLoss
loss_fn = SimpleContrastiveLoss()
gc = GradCache(
models=[encoder1, encoder2],
chunk_sizes=2,
loss_fn=loss_fn,
get_rep_fn=lambda v: v.pooler_output
)
在這裡,我們使用get_rep_fn參數指定一個函數,該函數獲取通用的擁抱面模型輸出並返回實際表示量張量。
創建模型輸入,
xx = tokenizer(tt, return_tensors='pt', padding=True)
yy = tokenizer(tt2, return_tensors='pt', padding=True)
運行緩存步驟,
gc(xx, yy, reduction='mean')
在這裡,我們使用reduction='mean'作為損失_KWARGS來控制損失行為。使用定義的optimizer ,可以完成完整的梯度更新
optimizer.zero_grad()
gc(xx, yy, reduction='mean')
optimizer.step()
這是由動態圖的(魔術)自然處理的。您將同一編碼器模型的淺副本傳遞給Gradcache Init方法。
tied_encoder = AutoModel.from_pretrained("bert-base-uncased").cuda()
gc = GradCache(
models=[tied_encoder , tied_encoder],
chunk_sizes=2,
loss_fn=loss_fn,
get_rep_fn=lambda v: v.pooler_output
)
在引擎蓋下,將註冊不同的鉤子以進行正確的梯度計算。
我們預計表示表示形式的交叉流程通信將由loss_fn處理。
from grad_cache.loss import DistributedContrastiveLoss
loss_fn_dist = DistributedContrastiveLoss()
正確包裝編碼器模型以減少梯度,
encoder1_ddp = DistributedDataParallel(
encoder1, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
encoder2_ddp = DistributedDataParallel(
encoder2, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
您可以初始化緩存使用分佈式損失和DDP模型,
gc = GradCache(
models=[encoder1_ddp, encoder2_ddp],
chunk_sizes=2,
loss_fn=loss_fn_dist,
get_rep_fn=lambda v: v.pooler_output
)
運行緩存步驟,
gc(xx, yy, no_sync_except_last=True, reduction='mean')
設置no_sync_except_last=True ,以避免不必要的梯度減少。
如果您正在開發一個新項目,我們建議還要檢查我們提供的裝飾器,以創建緩存的更高訂單功能。
grad_cache.functional.cached(func: Callable[..., Tensor])
將模型調用功能帶入緩存的兼容版本的裝飾器。
func-調用模型並返回表示張量的函數。
返回- 返回的函數1)表示緩存結構的葉片張量,2)第二個正向和後退的閉合函數。在損失張量後向後打電話後,請致電2)用1)作為參數。
grad_cache.functional.cat_input_tensor(func: Callable[..., Tensor])
將類型列表的位置和關鍵字參數(張量)連接到0位維度的單個張量中的裝飾器。這可能會派上用場,以處理來自多個緩存前進的代表性張量結果。
功能- 損失功能
返回- 緩存結果的裝飾損失功能。
grad_cache.functional.gather_input_tensor(func: Callable[..., Tensor], axis=0)
張量的所有聚集位置和關鍵字參數的裝飾器,並在軸上串聯。旨在用於創造分佈式對比度學習損失。
功能- 損失功能
返回- 分佈式培訓的裝飾損失功能。
如果您的數據加載程序發射小批量,則功能裝飾器特別有用,您可以從中構造大批批次。假設您也想進行自動混合精度,我們首先定義模型調用功能和丟失功能,
from grad_cache.functional import cached, cat_input_tensor
import torch
import torch.nn.functional as F
from torch.cuda.amp import autocast
@cached
@autocast()
def call_model(model, input):
return model(**input).pooler_output
@cat_input_tensor
@autocast()
def contrastive_loss(x, y):
target = torch.arange(0, y.size(0), int(y.size(0) / x.size(0)), device=x.device)
scores = torch.matmul(x, y.transpose(0, 1))
return F.cross_entropy(scores, target=target)
假設您有一個數據加載器loader ,該加載程序會散發出一小批元組(xx, yy)的大小(m * n),並且您想通過匯總16個小批次來訓練以獲取一批(16m * 16n),
cache_x = []
cache_y = []
closures_x = []
closures_y = []
for step, sub_batch in enumerate(loader):
xx, yy = sub_batch
rx, cx = call_model(bert, xx)
ry, cy = call_model(bert, yy)
cache_x.append(rx)
cache_y.append(ry)
closuresx.append(cx)
closuresy.append(cy)
if (step + 1) % 16 == 0:
loss = contrastive_loss(cache_x, cache_y)
scaler.scale(loss).backward()
for f, r in zip(closuresx, cache_x):
f(r)
for f, r in zip(closuresy, cache_y):
f(r)
cache_x = []
cache_y = []
closures_x = []
closures_y = []
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
運行分佈式多進程培訓要求:1)(ALL-)跨設備收集表示形式,以及跨設備的梯度。這兩個步驟都將發生在緩存的裝飾功能之外。
通過包裝編碼器(例如bert ),在DistributedDataParallel中可以易於實現。
bert = DistributedDataParallel(
bert, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
前者需要在損失函數中進行額外的分佈式操作,應根據原始損失定義進行。例如,
from torch import distributed as dist
from grad_cache.functional import cat_input_tensor, gather_input_tensor
@cat_input_tensor
@gather_input_tensor
@autocast()
def contrastive_loss(x, y):
target = torch.arange(0, y.size(0), int(y.size(0) / x.size(0)), device=x.device)
scores = torch.matmul(x, y.transpose(0, 1))
# scale the loss as DistributedDataParallel will do mean reduce
return F.cross_entropy(scores, target=target) * dist.get_world_size()
grad_cache/grad_cache.py-定義gradcache類。該代碼不到300行,包括評論。為了開發,我們鼓勵您閱讀它。
grad_cache/functional.py-定義裝飾器為從普通模型調用功能和損失功能中梯度緩存創建高階功能。