Spaces:
Runtime error
Runtime error
<!--Copyright 2021 The HuggingFace Team. All rights reserved. | |
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with | |
the License. You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | |
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | |
specific language governing permissions and limitations under the License. | |
--> | |
# Debugging | |
## Debug de problemas de Network multi-GPU | |
Cuando entrenas o infieres con `DistributedDataParallel` y varias GPUs, si encuentras problemas de intercomunicación entre procesos y/o nodos, puedes usar el siguiente script para diagnosticar problemas de red. | |
```bash | |
wget https://raw.githubusercontent.com/huggingface/transformers/main/scripts/distributed/torch-distributed-gpu-test.py | |
``` | |
Por ejemplo, para probar cómo interactúan 2 GPUs, haz lo siguiente: | |
```bash | |
python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py | |
``` | |
Si ambos procesos pueden hablar entre sí y asignar la memoria de la GPU, cada uno imprimirá un status OK. | |
Para más GPUs o nodos, ajusta los argumentos en el script. | |
Encontrarás muchos más detalles dentro del script de diagnóstico e incluso una receta de cómo ejecutarlo en un entorno SLURM. | |
Un nivel adicional de debug es agregar la variable de entorno `NCCL_DEBUG=INFO` de la siguiente manera: | |
```bash | |
NCCL_DEBUG=INFO python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py | |
``` | |
Esto mostrará mucha información de debug relacionada con NCCL, que luego puedes buscar online si encuentras que reporta algún problema. O si no estás seguro de cómo interpretar el output, puedes compartir el archivo de log en un Issue. | |
## Detección de Underflow y Overflow | |
<Tip> | |
Esta función está disponible actualmente sólo para PyTorch. | |
</Tip> | |
<Tip> | |
Para el entrenamiento multi-GPU, requiere DDP (`torch.distributed.launch`). | |
</Tip> | |
<Tip> | |
Esta función puede utilizarse con cualquier modelo basado en `nn.Module`. | |
</Tip> | |
Si empiezas a obtener `loss=NaN` o el modelo muestra algún otro comportamiento anormal debido a `inf` o `nan` en | |
activations o weights hay que descubrir dónde se produce el primer underflow o overflow y qué lo ha provocado. Por suerte | |
puedes lograrlo fácilmente activando un módulo especial que hará la detección automáticamente. | |
Si estás usando [`Trainer`], solo necesitas añadir: | |
```bash | |
--debug underflow_overflow | |
``` | |
a los argumentos normales de la línea de comandos, o pasar `debug="underflow_overflow"` al crear el objeto [`TrainingArguments`]. | |
Si estás usando tu propio bucle de entrenamiento u otro Trainer puedes lograr lo mismo con: | |
```python | |
from .debug_utils import DebugUnderflowOverflow | |
debug_overflow = DebugUnderflowOverflow(model) | |
``` | |
[`~debug_utils.DebugUnderflowOverflow`] inserta hooks en el modelo que inmediatamente después de cada forward | |
testeará las variables de input y output y también los weights del módulo correspondiente. Tan pronto como se detecte `inf` o | |
`nan` se detecta en al menos un elemento de las activations o weights, el programa afirmará e imprimirá un informe | |
como este (esto fue capturado con `google/mt5-small` bajo fp16 mixed precision): | |
``` | |
Detected inf/nan during batch_number=0 | |
Last 21 forward frames: | |
abs min abs max metadata | |
encoder.block.1.layer.1.DenseReluDense.dropout Dropout | |
0.00e+00 2.57e+02 input[0] | |
0.00e+00 2.85e+02 output | |
[...] | |
encoder.block.2.layer.0 T5LayerSelfAttention | |
6.78e-04 3.15e+03 input[0] | |
2.65e-04 3.42e+03 output[0] | |
None output[1] | |
2.25e-01 1.00e+04 output[2] | |
encoder.block.2.layer.1.layer_norm T5LayerNorm | |
8.69e-02 4.18e-01 weight | |
2.65e-04 3.42e+03 input[0] | |
1.79e-06 4.65e+00 output | |
encoder.block.2.layer.1.DenseReluDense.wi_0 Linear | |
2.17e-07 4.50e+00 weight | |
1.79e-06 4.65e+00 input[0] | |
2.68e-06 3.70e+01 output | |
encoder.block.2.layer.1.DenseReluDense.wi_1 Linear | |
8.08e-07 2.66e+01 weight | |
1.79e-06 4.65e+00 input[0] | |
1.27e-04 2.37e+02 output | |
encoder.block.2.layer.1.DenseReluDense.dropout Dropout | |
0.00e+00 8.76e+03 input[0] | |
0.00e+00 9.74e+03 output | |
encoder.block.2.layer.1.DenseReluDense.wo Linear | |
1.01e-06 6.44e+00 weight | |
0.00e+00 9.74e+03 input[0] | |
3.18e-04 6.27e+04 output | |
encoder.block.2.layer.1.DenseReluDense T5DenseGatedGeluDense | |
1.79e-06 4.65e+00 input[0] | |
3.18e-04 6.27e+04 output | |
encoder.block.2.layer.1.dropout Dropout | |
3.18e-04 6.27e+04 input[0] | |
0.00e+00 inf output | |
``` | |
El output del ejemplo se ha recortado en el centro por razones de brevedad. | |
La segunda columna muestra el valor del elemento más grande en términos absolutos, por lo que si observas con detenimiento los últimos fotogramas, | |
los inputs y outputs estaban en el rango de `1e4`. Así que cuando este entrenamiento se hizo con fp16 mixed precision, | |
el último paso sufrió overflow (ya que bajo `fp16` el mayor número antes de `inf` es `64e3`). Para evitar overflows en | |
`fp16` las activations deben permanecer muy por debajo de `1e4`, porque `1e4 * 1e4 = 1e8` por lo que cualquier matrix multiplication con | |
grandes activations va a llevar a una condición de overflow numérico. | |
Al principio del output puedes descubrir en qué número de batch se produjo el problema (aquí `Detected inf/nan during batch_number=0` significa que el problema se produjo en el primer batch). | |
Cada frame del informe comienza declarando la entrada completamente calificada para el módulo correspondiente que este frame está reportando. | |
Si nos fijamos sólo en este frame: | |
``` | |
encoder.block.2.layer.1.layer_norm T5LayerNorm | |
8.69e-02 4.18e-01 weight | |
2.65e-04 3.42e+03 input[0] | |
1.79e-06 4.65e+00 output | |
``` | |
Aquí, `encoder.block.2.layer.1.layer_norm` indica que era una layer norm para la primera capa, del segundo | |
block del encoder. Y la call específica del `forward` es `T5LayerNorm`. | |
Veamos los últimos frames de ese informe: | |
``` | |
Detected inf/nan during batch_number=0 | |
Last 21 forward frames: | |
abs min abs max metadata | |
[...] | |
encoder.block.2.layer.1.DenseReluDense.wi_0 Linear | |
2.17e-07 4.50e+00 weight | |
1.79e-06 4.65e+00 input[0] | |
2.68e-06 3.70e+01 output | |
encoder.block.2.layer.1.DenseReluDense.wi_1 Linear | |
8.08e-07 2.66e+01 weight | |
1.79e-06 4.65e+00 input[0] | |
1.27e-04 2.37e+02 output | |
encoder.block.2.layer.1.DenseReluDense.wo Linear | |
1.01e-06 6.44e+00 weight | |
0.00e+00 9.74e+03 input[0] | |
3.18e-04 6.27e+04 output | |
encoder.block.2.layer.1.DenseReluDense T5DenseGatedGeluDense | |
1.79e-06 4.65e+00 input[0] | |
3.18e-04 6.27e+04 output | |
encoder.block.2.layer.1.dropout Dropout | |
3.18e-04 6.27e+04 input[0] | |
0.00e+00 inf output | |
``` | |
El último frame informa para la función `Dropout.forward` con la primera entrada para el único input y la segunda para el | |
único output. Puedes ver que fue llamada desde un atributo `dropout` dentro de la clase `DenseReluDense`. Podemos ver | |
que ocurrió durante la primera capa, del segundo block, durante el primer batch. Por último, el mayor absoluto | |
elementos de input fue `6.27e+04` y el mismo para el output fue `inf`. | |
Puedes ver aquí, que `T5DenseGatedGeluDense.forward` resultó en output activations, cuyo valor máximo absoluto fue | |
alrededor de 62.7K, que está muy cerca del límite máximo de fp16 de 64K. En el siguiente frame tenemos `Dropout`, el cual renormaliza | |
los weights, después de poner a cero algunos de los elementos, lo que empuja el valor máximo absoluto a más de 64K, y obtenemos un | |
overflow (`inf`). | |
Como puedes ver son los frames anteriores los que tenemos que mirar cuando los números empiezan a ser muy grandes para números fp16. | |
Combinemos el informe con el código de `models/t5/modeling_t5.py`: | |
```python | |
class T5DenseGatedGeluDense(nn.Module): | |
def __init__(self, config): | |
super().__init__() | |
self.wi_0 = nn.Linear(config.d_model, config.d_ff, bias=False) | |
self.wi_1 = nn.Linear(config.d_model, config.d_ff, bias=False) | |
self.wo = nn.Linear(config.d_ff, config.d_model, bias=False) | |
self.dropout = nn.Dropout(config.dropout_rate) | |
self.gelu_act = ACT2FN["gelu_new"] | |
def forward(self, hidden_states): | |
hidden_gelu = self.gelu_act(self.wi_0(hidden_states)) | |
hidden_linear = self.wi_1(hidden_states) | |
hidden_states = hidden_gelu * hidden_linear | |
hidden_states = self.dropout(hidden_states) | |
hidden_states = self.wo(hidden_states) | |
return hidden_states | |
``` | |
Ahora es fácil ver la call `dropout`, y también todas las calls anteriores. | |
Dado que la detección se produce en un forward hook, estos informes se imprimen inmediatamente después de que cada `forward` | |
responda. | |
Volviendo al informe completo, para actuar sobre él y arreglar el problema, tenemos que subir unos cuantos frames donde los números | |
empezaron a subir y probablemente cambiar al modo `fp32` aquí, para que los números no sufran overflow cuando se multipliquen | |
o al sumarlos. Por supuesto, puede haber otras soluciones. Por ejemplo, podríamos desactivar `amp` temporalmente si está | |
activado, después de mover el original `forward` dentro de un helper wrapper, así: | |
```python | |
def _forward(self, hidden_states): | |
hidden_gelu = self.gelu_act(self.wi_0(hidden_states)) | |
hidden_linear = self.wi_1(hidden_states) | |
hidden_states = hidden_gelu * hidden_linear | |
hidden_states = self.dropout(hidden_states) | |
hidden_states = self.wo(hidden_states) | |
return hidden_states | |
import torch | |
def forward(self, hidden_states): | |
if torch.is_autocast_enabled(): | |
with torch.cuda.amp.autocast(enabled=False): | |
return self._forward(hidden_states) | |
else: | |
return self._forward(hidden_states) | |
``` | |
Como el detector automático sólo informa de los inputs y outputs de los frames completos, una vez que sepas dónde buscar, puedes | |
analizar también las etapas intermedias de una función específica de `forward`. En este caso, puede utilizar la función | |
función de ayuda `detect_overflow` para inyectar el detector donde quieras, por ejemplo: | |
```python | |
from debug_utils import detect_overflow | |
class T5LayerFF(nn.Module): | |
[...] | |
def forward(self, hidden_states): | |
forwarded_states = self.layer_norm(hidden_states) | |
detect_overflow(forwarded_states, "after layer_norm") | |
forwarded_states = self.DenseReluDense(forwarded_states) | |
detect_overflow(forwarded_states, "after DenseReluDense") | |
return hidden_states + self.dropout(forwarded_states) | |
``` | |
Puedes ver que hemos añadido 2 de estos y ahora se trackea si `inf` o `nan` para `forwarded_states` fue detectado | |
en algún punto intermedio. | |
De hecho, el detector ya informa de esto porque cada una de las llamadas en el ejemplo anterior es un `nn.Module`, pero | |
digamos que si tuvieras algunos cálculos directos locales, así es como lo harías. | |
Además, si estás instanciando el debugger en tu propio código, puedes ajustar el número de frames impresos de | |
su valor por defecto, por ejemplo: | |
```python | |
from .debug_utils import DebugUnderflowOverflow | |
debug_overflow = DebugUnderflowOverflow(model, max_frames_to_save=100) | |
``` | |
### Rastreo de valores mínimos y máximos absolutos de batches específicos | |
La misma clase de debugging se puede utilizar para el rastreo por batches con la función de detección de underflow/overflow desactivada. | |
Digamos que quieres ver los valores mínimos y máximos absolutos de todos los ingredientes de cada call `forward` de un determinado | |
batch, y sólo hacerlo para los batches 1 y 3. Entonces instancias esta clase como: | |
```python | |
debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3]) | |
``` | |
Y ahora los batches 1 y 3 completos serán rastreados usando el mismo formato que el detector de underflow/overflow. | |
Los batches son 0-index. | |
Esto es muy útil si sabes que el programa empieza a comportarse mal después de un determinado número de batch, para que puedas avanzar rápidamente | |
hasta esa área. Aquí hay un ejemplo de output recortado para tal configuración: | |
``` | |
*** Starting batch number=1 *** | |
abs min abs max metadata | |
shared Embedding | |
1.01e-06 7.92e+02 weight | |
0.00e+00 2.47e+04 input[0] | |
5.36e-05 7.92e+02 output | |
[...] | |
decoder.dropout Dropout | |
1.60e-07 2.27e+01 input[0] | |
0.00e+00 2.52e+01 output | |
decoder T5Stack | |
not a tensor output | |
lm_head Linear | |
1.01e-06 7.92e+02 weight | |
0.00e+00 1.11e+00 input[0] | |
6.06e-02 8.39e+01 output | |
T5ForConditionalGeneration | |
not a tensor output | |
*** Starting batch number=3 *** | |
abs min abs max metadata | |
shared Embedding | |
1.01e-06 7.92e+02 weight | |
0.00e+00 2.78e+04 input[0] | |
5.36e-05 7.92e+02 output | |
[...] | |
``` | |
Aquí obtendrás un gran número de frames mostrados - tantos como forward calls haya en tu modelo, por lo que puede o no ser lo que quieras, pero a veces puede ser más fácil de usar para debug que un debugger normal. | |
Por ejemplo, si un problema comienza a ocurrir en el batch 150. Entonces puedes mostrar las trazas de los batches 149 y 150 y comparar dónde | |
los números empezaron a divergir. | |
También puedes especificar el número de batch después del cual se debe detener el entrenamiento, con: | |
```python | |
debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3], abort_after_batch_num=3) | |
``` | |