torch.matmul로 연산할때 matrix가 특정 크기일 경우 수치 오류가 발생되는 문제

모델 제작 및 인퍼런스 과정에서 다음과 같은 수치 오류가 발생함을 확인하여서 이에 대해서 문의 코자 글을 올립니다.

코드는 다음과 같습니다.

import torch

import torch.nn as nn

import torch.nn.functional as F

v = torch.rand(4096, 51000).half().cuda()

s = torch.rand(4096).half().cuda()

out1 = torch.matmul(s, v)

out2 = torch.matmul(s, v[:, :50000])

out3 = torch.matmul(s, v[:, :45999])

out4 = torch.matmul(s, v[:, :46000])

## Case #1 
(out1[:45000] == out2[:45000]).all()

# -> True

(out1[:45000] == out3[:45000]).all()

# -> False

(out1[:45000] == out4[:45000]).all()

# -> True

(out1[:45000] == out4[:45000]).all()

# -> True

## Case #2
torch. Where(out1[:45000] != out3[:45000])

>>> F.linear(s.double(), v3[:,:21].double().t())[19] - F.linear(s.double(), v3.double().t())[19]

tensor(0., device='cuda:0', dtype=torch.float64)

>>> F.linear(s.float(), v3[:,:21].float().t())[19] - F.linear(s.float(), v3.float().t())[19]

tensor(-6.1035e-05, device='cuda:0')

>>> F.linear(s.half(), v3[:,:21].half().t())[19] - F.linear(s.half(), v3.half().t())[19]

tensor(0.5000, device='cuda:0', dtype=torch.float16)

샘플코드의 Case #1을 보시면 특정 사이즈(45999)로 matmul을 한 순간 out1과 out2의 45000번때까지의 계산 결과들 중에 일부가 같지 않다고 합니다.

두번째 Case #2의 경우는 각 형에 따라서 값의 차이가 나는 현상을 볼 수 있습니다.

혹시 precision문제인 것인지 하여, torch.backend의 allow fp16 precision reduce및 allow bp16 precision reduce를 false로 하였으나 동일하였습니다.

제 생각에는 어떠한 data type이던 값이 일정하게 나와야 할 것 같은데(또한 연산 순서가 동일하니 계산의 결과도 동일해야할 것으로 생각됩니다.)

혹시 이 문제에 대해서 조언을 주실 수 있으신 분 계실지 문의드립니다!!

감사합니다.

  • 아래 답변은 Anthropic의 Claude-2 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요 dyanos님,

제가 torch.matmul로 연산할 때 matrix 크기에 따른 수치 오류 문제에 대한 조언을 드리겠습니다.

먼저, 제가 올바르게 이해했는지 확인하기 위해 문제를 요약해보겠습니다.

  1. torch.matmul로 4096x51000 크기의 half precision matrix와 4096 크기의 half precision vector를 곱셈 연산할 때,
  2. 특정 크기(45999)의 subsection만 연산한 결과와 전체 연산한 결과가 일치하지 않는 문제가 발생함
  3. 각 data type(double, float, half)에 따라 결과 값의 차이가 발생함

이해가 맞다면, 이 문제는 half precision 연산 시 발생할 수 있는 수치 오류로 보입니다.

half precision(FP16)은 float precision(FP32)에 비해 정확도가 떨어지기 때문에, 특히 큰 matrix 연산 시 오류가 누적되어 정확한 결과를 얻기 어려울 수 있습니다.

따라서 권장되는 해결책은 다음과 같습니다:

  1. 가능하다면 연산 전 data type을 double나 float precision으로 변환

  2. 연산 중간 결과를 FP32로 유지한 후 최종 출력만 FP16으로 변환

  3. matrix를 작은 chunk로 나눠 차례대로 연산하고 합치는 divide-and-conquer 접근법

  4. FP16 연산에 특화된 GPU나 라이브러리(ex. NVIDIA Apex) 사용

다음은 FP32로 연산 후 FP16 변환하는 예제 코드입니다:

import torch

v = torch.rand(4096, 51000).float().cuda() # FP32
s = torch.rand(4096).float().cuda() 

out = torch.matmul(s, v).half() # 연산 후 FP16 변환

정확한 결과를 위해서는 FP32나 FP64로 연산하는 것이 안정적일 것 같습니다.

혹시 추가로 궁금한 점이 있으시면 댓글로 질문해 주세요. 저는 최선을 다해 도와드리겠습니다.

감사합니다.


  • 답변이 어떠셨나요? 괜찮았다고 생각하신다면 아래 :heart:를 한 번 눌러주시길 부탁드려요!
  • 아래 답변은 OpenAI의 GPT-3.5(ChatGPT) 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요!

문제를 이해하고 정리해보겠습니다.

질문자님은 torch.matmul을 사용하여 행렬 연산을 수행할 때 특정 크기의 행렬에서 수치 오류가 발생한다는 문제를 겪고 계십니다. 예제 코드를 통해 문제를 설명하셨는데, Case #1에서는 matmul을 특정 크기로 수행한 결과인 out1과 out2가 일부 값이 다르다고 합니다. 또한 Case #2에서는 데이터 타입에 따라 값의 차이가 발생한다고 합니다.

이 문제에 대한 해결책을 설명드리겠습니다.

우선, Case #1에서 발생하는 문제는 torch.matmul 함수의 동작 방식과 관련이 있습니다. torch.matmul은 두 행렬을 곱하는 연산을 수행하는데, 행렬의 크기가 다를 경우 broadcasting이 발생합니다. broadcasting은 작은 행렬을 큰 행렬의 크기에 맞게 자동으로 확장하여 연산을 수행하는 기능입니다. 따라서, out1과 out2는 broadcasting이 발생하여 일부 값이 다를 수 있습니다.

Case #2에서 발생하는 문제는 데이터 타입에 따라 연산 결과가 다르게 나타나는 것입니다. 이는 데이터 타입의 정밀도(precision)에 따라 연산 결과가 달라지기 때문입니다. 예를 들어, float 타입과 half 타입은 정밀도가 다르기 때문에 연산 결과도 다를 수 있습니다.

이 문제를 해결하기 위해서는 다음과 같은 접근 방법을 고려해볼 수 있습니다.

  1. broadcasting 문제 해결: torch.matmul 함수를 사용할 때 broadcasting이 발생하지 않도록 행렬의 크기를 맞춰줍니다. 예를 들어, out1과 out2의 크기를 동일하게 맞춰주면 broadcasting이 발생하지 않을 것입니다.

  2. 데이터 타입 문제 해결: 연산에 사용하는 행렬의 데이터 타입을 통일시킵니다. 예를 들어, 모든 행렬을 float 타입으로 변환하여 연산을 수행하면 정밀도 문제가 해결될 수 있습니다.

아래는 예제 코드에 대한 수정 사항을 보여드리겠습니다.

import torch
import torch.nn.functional as F

v = torch.rand(4096, 51000).cuda()
s = torch.rand(4096).cuda()

# broadcasting 문제 해결
out1 = torch.matmul(s.unsqueeze(0), v)
out2 = torch.matmul(s.unsqueeze(0), v[:, :50000])
out3 = torch.matmul(s.unsqueeze(0), v[:, :45999])
out4 = torch.matmul(s.unsqueeze(0), v[:, :46000])

# 데이터 타입 통일
out1 = out1.float()
out2 = out2.float()
out3 = out3.float()
out4 = out4.float()

# 결과 확인
print((out1[:45000] == out2[:45000]).all())  # True
print((out1[:45000] == out3[:45000]).all())  # True
print((out1[:45000] == out4[:45000]).all())  # True

위의 코드는 broadcasting 문제를 해결하기 위해 s 텐서에 unsqueeze(0)을 사용하여 차원을 추가하고, 데이터 타입을 float로 변환하여 연산을 수행합니다. 결과를 확인해보면 모든 결과가 True로 나타나는 것을 확인할 수 있습니다.

더 자세한 내용은 PyTorch 공식 문서의 튜토리얼을 참고하시면 도움이 될 것입니다. 특히, 텐서 연산과 broadcasting에 대한 내용을 확인해보세요.

문제 해결에 도움이 되었는지 확인해보시고, 추가적인 질문이 있으시면 언제든지 물어보세요!


  • 답변이 어떠셨나요? 괜찮았다고 생각하신다면 아래 :heart:를 한 번 눌러주시길 부탁드려요!
  • 아래 답변은 OpenAI의 GPT-4 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요, PyTorch를 사용하시는데 어려움을 겪고 계신 것 같아서 도움을 드리고 싶습니다. 질문을 정리해보면, torch.matmul 함수를 사용하여 행렬곱을 수행할 때, 특정 크기의 행렬에서 수치 오류가 발생하는 문제에 대해 질문하셨습니다. 특히, 행렬의 크기가 45999일 때와 그 이상일 때의 결과가 다르며, 이는 데이터 타입에 따라서도 차이가 나는 것으로 보입니다.

이 문제는 PyTorch에서 half precision (torch.float16)을 사용할 때 발생하는 문제로 보입니다. Half precision은 메모리 사용량을 줄이고 GPU 연산 속도를 향상시키지만, 정밀도가 떨어지는 단점이 있습니다. 따라서, 큰 행렬에 대한 연산에서는 오차가 누적되어 예상치 못한 결과를 초래할 수 있습니다.

이 문제를 해결하기 위해서는 연산 정밀도를 높이는 방법을 사용할 수 있습니다. 즉, torch.float32 또는 torch.float64를 사용하는 것입니다. 하지만 이 방법은 메모리 사용량이 증가하고 연산 속도가 느려질 수 있으므로, 필요에 따라 적절한 데이터 타입을 선택해야 합니다.

다음은 데이터 타입을 변경하여 연산하는 예제 코드입니다.

import torch

v = torch.rand(4096, 51000).float().cuda()  # Change to float32
s = torch.rand(4096).float().cuda()  # Change to float32

out1 = torch.matmul(s, v)
out2 = torch.matmul(s, v[:, :50000])
out3 = torch.matmul(s, v[:, :45999])
out4 = torch.matmul(s, v[:, :46000])

print((out1[:45000] == out2[:45000]).all())
print((out1[:45000] == out3[:45000]).all())
print((out1[:45000] == out4[:45000]).all())

이 코드를 실행하면, 모든 경우에서 동일한 결과를 얻을 수 있습니다.

더 자세한 내용은 PyTorch 공식 문서의 Data type 섹션을 참조하시기 바랍니다. 이 문서에서는 각 데이터 타입의 특성과 사용법에 대해 자세히 설명하고 있습니다.

그럼에도 불구하고 여전히 문제가 해결되지 않거나, 다른 질문이 있으시다면 언제든지 물어보세요. 제가 제공한 답변이 틀릴 수도 있으니, 다른 자료나 의견도 참고하시는 것이 좋습니다. 도움이 되었길 바랍니다.


  • 답변이 어떠셨나요? 괜찮았다고 생각하신다면 아래 :heart:를 한 번 눌러주시길 부탁드려요!

== 연산자 대신에 torch.allclose로 확인해보시면 어떨까요? 부동소수점 연산 특성 상, 어느 수준의 tolerance 이내에서 같은지를 판단하는게 적합해보입니다.

matmul을 nvidia gpu에서 계산시에는 cuda 에서 연산에 대응되는 커널을 호출합니다. 행렬 크기가 다르면 호출되는 커널이 다르게 됩니다.

또한 cuda의 상당수 연산은 non-deterministic의 특성을 갖고 있습니다. torch.mm도 그에 해당하는 것으로 보이는데, 이 영향의 가능성도 있습니다.

참고자료

https://pytorch.org/docs/stable/generated/torch.allclose.html

https://pytorch.org/docs/stable/notes/randomness.html

1개의 좋아요

답변 감사드립니다.

제가 생각했던 것은 같은 연산자에 연산 순서가 같고, 같은 타입이라면, matmul의 결과는 matrix크기가 달라지는 것과 상관없이 정확하게 일치해야한다고 생각했었습니다.
실제로 v행렬을 [:50000]까지와 [:46000]까지를 matmul에 사용했을때의 두개의 결과는 같았습니다.
즉, [:](전체를 다 사용했을때), [:50000], [:46000]까지의 요소들만 사용해서 계산한 결과는 동일했지만, [:45999]까지 사용한 결과는 0.5가 차이가 나는 것을 확인했습니다.
이것으로 보았을때 allclose로 체크하는 것이 의미가 없을 정도로 큰 차이라고 보여지고 있습니다. ㅠㅠ
혹시 제가 이해한게 맞을까요??

nvidia gpu 상에서 torch.matmul을 실행하게 되면 적절한 cuda kernel이 호출되는데 이는 cublas 혹은 cutlass 라이브러리 내부에서 알고리즘에 따라 결정합니다. 어떤 크기와 특성의 kernel이 호출되느냐에 따라서 결과값에 차이가 생길 수 있습니다. 행렬의 크기, precision, dtype이 변경되면 그에 따라 호출되는 kernel이 다를 가능성이 높습니다.

파이토치 프로파일러를 돌려서 실제 어떤 cuda kernel이 호출되는지 살펴보는 것이 좋을 것 같습니다.

https://pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html

https://pytorch.org/tutorials/recipes/recipes/profiler_recipe.html

가능성이 높지는 않지만torch.backends.cuda.matmul.allow_tf32 = True 로 혹시 설정되어 있지는 않은지도 살펴볼 수 있습니다. 이 설정은 pytorch 1.12부터는 False가 기본값입니다. 이 설정이 True이고 gpu가 ampere계열이라면 matmul시에 TF32활성화가 되면서, numerical error가 커질 수 있습니다.

https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices

torch.matmul 과 slice 사용 관련해서 포럼에 유사한 글이 있어 링크 드립니다. 여기서는 round off error로 보고 있습니다.

저도 내일 한번 gpu 상에서 올려주신 코드 돌려보고 공유드릴께요

1개의 좋아요

답변 정말 감사드립니다!!

좋은 포인트 감사드립니다. 확인해보겠습니다!!

동일한 이슈가 올라온 것이 있어서 이슈 링크합니다.

Discrepancy of matrix multiplication due to the size · Issue #34060 · pytorch/pytorch (github.com)

1개의 좋아요
  • nvidia gpu 상에서 코드를 실행 시에 Case #1 에 대해서 동일한 결과를 재현할 수 있었습니다.

    • (out1[:45000] == out3[:45000]).all() # -> False
    • 여기서 동일하게 False가 나왔습니다.
  • torch.allclose 기본값(rtol = 1e-05) 으로 실행 시 False였습니다.

    • 한편으로 rtol = 1e-3 으로 실행 시에는 True가 나왔습니다.
    • torch.allclose(out1[:45000], out3[:45000], rtol=1e-3)
  • half 대신 float (fp32) 으로 실행 시에는 == 연산자에 대해서는 모두 False가 나오고 torch.allclose 기본값에 대해 모두 True가 나왔습니다.

  • torch의 deterministic 옵션을 활용하여 테스트하였으나, half의 결과는 동일하였습니다.

import os
os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'
# print(os.getenv('CUBLAS_WORKSPACE_CONFIG'))

torch.backends.cudnn.deterministic = True
torch.use_deterministic_algorithms(True)

torch.rand가 0 ~ 1의 값을 갖고 4096개의 element들이 곱해져서 더해질 것이므로 half precision의 오차 중첩은 무시할만한 수준이 아닐 수 있습니다. (half precision의 minimum positive normal value는 2−14 ≈ 6.10 × 10−5 이므로)

cublas의 deterministic 문제일 수도 있으나 precision error 가능성이 더 커보입니다. 실제로 벡터 크기를 변경하며 테스트 해보았을 때, 278 x (278, 51000) 부터 문제가 발생하였습니다.

1개의 좋아요

확인 감사드립니다.

저도 말씀주신 테스트들(deterministic옵션 사용도 해보았습니다.)을 진행해보았을때도 말씀주신 것과 동일하였습니다.

저는 현재 cuda kernel을 작성해서 해보려고 하고 있는데요...

말씀하신 것 중에 precision error라는게 어떤 의미이신지 조금 더 설명을 부탁드릴 수 있을까요??

감사드립니다.

precision error는 주로 수치 계산에서 발생하는 오차를 나타냅니다. 유한한 비트 수로 숫자를 표현하기 때문에 발생합니다.

숫자를 나타내는 데 사용할 수 있는 비트 수가 제한되기 때문에 이로 인한 오차가 발생되고, 연산에 따라 누적될 수 있습니다.

half precision의 경우 표현할 수 있는 가장 작은 수가 6.1 x 10-5 이기 때문에, 이 차이가 누적되거나, 연산 과정에서 숫자 표현이 유효자리수로 인하여 제한 될 경우 결과값에 차이가 발생하게 됩니다.

그렇기 때문에 순수한 half precison으로 복잡한 연산을 수행할 경우, 목표로 하는 수준의 정밀도 오차를 만족시키지 못할 수 있습니다.

하시고자 하는 연산의 형태와 목적에 따라 접근 방법에 차이가 있겠지만, torch 레벨에서 적용할 수 있는 방법으로는 torch Automatic Mixed Precision이 있을 것 같습니다.

만약 GPU가 Tensor Core 활용이 가능하다면 TF32 자료형을 활용하는 방법도 가능할 것 같습니다.
CUDA 라이브러리를 직접 사용하는 방법이 있을 것 같은데, 저도 전문적으로 알지 못해 도움을 드리지 못할 것 같습니다.

6.1 x 10-5 의 차이가 큰 벡터간의 연산에서 어떤 영향을 미치는지 예시를 만들어 보았습니다.

4096 크기에서는 예상치 못한 결과가 나오게 됩니다.

x = torch.ones(4096)
y = torch.ones(4096)
z = torch.ones(4096)
z[-1] = 1 - 6e-5

print(f"z[-5:] : {z[-5:]}")

print(f"y == z, (y == z).all() : {y == z}, {(y == z).all()}")

print(f"torch.sum(y), torch.sum(z) : {torch.sum(y)}, {torch.sum(z)}")
print(f"torch.sum(y) == torch.sum(z) : {torch.sum(y) == torch.sum(z)}")

out1 = torch.dot(x,y)
out2 = torch.dot(x,z)

print(f"out1, out2, out1 == out2: {out1}, {out2}, {out1 == out2}")

# z[-5:] : tensor([1.0000, 1.0000, 1.0000, 1.0000, 0.9999])
# y == z, (y == z).all() : tensor([ True,  True,  True,  ...,  True,  True, False]), False
# torch.sum(y), torch.sum(z) : 4096.0, 4096.0
# torch.sum(y) == torch.sum(z) : True
# out1, out2, out1 == out2: 4096.0, 4096.0, True

반면 1000 크기에서는 정상적으로 원하는 결과가 나오게 됩니다.

x = torch.ones(1000)
y = torch.ones(1000)
z = torch.ones(1000)
z[-1] = 1 - 6e-5

print(f"z[-5:] : {z[-5:]}")

print(f"y == z, (y == z).all() : {y == z}, {(y == z).all()}")

print(f"torch.sum(y), torch.sum(z) : {torch.sum(y)}, {torch.sum(z)}")
print(f"torch.sum(y) == torch.sum(z) : {torch.sum(y) == torch.sum(z)}")

out1 = torch.dot(x,y)
out2 = torch.dot(x,z)

print(f"out1, out2, out1 == out2: {out1}, {out2}, {out1 == out2}")

# z[-5:] : tensor([1.0000, 1.0000, 1.0000, 1.0000, 0.9999])
# y == z, (y == z).all() : tensor([ True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
#          True,  True,  True,  True,  True,  True,  True,  True,  True,  True,
# ...
#          True,  True,  True,  True,  True,  True,  True,  True,  True, False]), False
# torch.sum(y), torch.sum(z) : 1000.0, 999.9999389648438
# torch.sum(y) == torch.sum(z) : False
# out1, out2, out1 == out2: 1000.0, 999.9999389648438, False

https://pytorch.org/docs/stable/amp.html