336 lines
14 KiB
Markdown
336 lines
14 KiB
Markdown
<!--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.
|
|
|
|
⚠️ Note that this file is in Markdown but contain specific syntax for our doc-builder (similar to MDX) that may not be
|
|
rendered properly in your Markdown viewer.
|
|
|
|
-->
|
|
|
|
# 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)
|
|
```
|