OOM(Out Of Memory)가 발생하는 이유

Linux에서는 프로세스 실행 시 필요한 메모리를 바로 할당하는 것이 아니라 메모리를 주었다는 것을 저장하는 Memory Commit 정책을 가지고 있다. 실제 메모리 할당은 프로세스가 실행되면서 메모리가 필요할 때 이루어진다.

그리고 Linux는 Overcommit를 허용하는데, 이는 커밋된 메모리가 실제 가용 메모리보다 더 큰 것을 뜻한다. Overcommit 덕분에 메모리 사용량이 큰 프로세스가 있어도 실행될 수가 있다. 그런데 Overcommit이 된 상태에서 프로세스에게 실제 메모리 할당이 되면 가용 메모리보다 더 큰 메모리를 할당할 수도 있는데, 이 때 OOM(Out Of Memory)이 발생하기 때문에 Overcommit은 OOM의 주요 원인으로 손꼽힌다.

OOM Killer과 OOM Score

OOM이 발생하면 메모리 확보를 위해 프로세스를 강제로 종료 시키는 OOM Killer가 실행된다. OOM Killer이 프로세스를 종료할 때 당연히 무작위로 종료하는 것은 아니고, OOM Score에 따라서 종료될 프로세스가 결정된다. OOM Score은 0~1000 사이의 값을 가지고, 값이 클 수록 종료 우선순위가 높아진다. 따라서 OOM Killer는 OOM Score이 높은 순서대로 프로세스를 종료한다.

OOM Score은 oom_badness()를 통해 계산된다. Heuristic하게 계산되는 점수인 만큼 시간이 지나면서 점수 계산 방법이 여러 번 바뀌었던 것 같은데 현재는 기본적으로 메모리 사용량이 높을 수록 OOM Score이 높아진다.

OOM Killer가 실행됐을 때 로그는 /var/log/syslog 에서 볼 수 있다.

OOM Score 확인법

Linux에서 프로세스의 OOM Score 관련 값이 저장되어 있는 경로는 다음과 같다.

  • /proc/{PID}/oom_score : 프로세스의 OOM Score(0~1000)
  • /proc/{PID}/oom_adj : OOM Score 조정값(-17 ~ 17). Deprecated
  • /proc/{PID}/oom_score_adj : OOM Score 조정값(-1000 ~ 1000)

oom_score_adj 값은 OOM Score 조정 값이다. OOM Score를 계산하는 oom_badness()를 보면 마지막에 adj이 OOM Score에 더해지는 것을 볼 수 있다. oom_score_adj 값이 -1000이면 OOM Score LONG_MIN 값을 가져서 종료되지 않는다.

따라서 하나의 머신에서 서버를 포함한 여러 개의 프로세스가 실행되어 있는데, OOM이 발생해도 OOM Killer가 서버 프로세스를 종료하지 않도록 하기 위해서는 서버 프로세스의 oom_score_adj 값을 -1000으로 바꾸면 된다.

예시

  • OOM Score 확인
$ cat /proc/2533032/oom_score
2
  • oom_score_adj 확인
$ cat /proc/2533032/oom_score_adj
-998
  • oom_score_adj
$ sudo echo 0 > oom_score_adj
  • oom_score_adj 값을 -998에서 0으로 변경

Reference

 

Huggingface hub에 모델을 올리는 법을 자꾸 까먹어서 글로 남겨보려고 합니다. 방법들을 찾아보니 총 3가지 방법이 있는거 같은데, 이들에 대해 다뤄보고자 합니다. 실습에 사용된 패키지와 버전은 아래와 같습니다.

transformers==4.14.1 
huggingface-hub==0.2.1 # transformers 설치하면 같이 설치됨

그리고 빠른 실습을 위해 모델의 용량이 작은 gpt2-tiny모델을 이용하였습니다.

1. push_to_hub() 이용

모델과 토크나이저에 대해서 push_to_hub()메소드를 이용하여 바로 모델을 업로드하는 방법이 있습니다. 이 방법을 이용하면 원격 저장소 생성까지 한 후에 업로드를 해줍니다.

from transformers import AutoTokenizer, AutoModelForCausalLM

print("model loading...")

# Model & Tokenizer loading
tokenizer = AutoTokenizer.from_pretrained("sshleifer/tiny-gpt2")
model = AutoModelForCausalLM.from_pretrained("sshleifer/tiny-gpt2")

# Repository 생성 & model upload
REPO_NAME = YOUR_REPO_NAME # ex) 'my-bert-fine-tuned'
AUTH_TOKEN = YOUR_OWN_TOKEN # <https://huggingface.co/settings/token>

## Upload to Huggingface Hub
model.push_to_hub(
    REPO_NAME, 
    use_temp_dir=True, 
    use_auth_token=AUTH_TOKEN
)
tokenizer.push_to_hub(
    REPO_NAME, 
    use_temp_dir=True, 
    use_auth_token=AUTH_TOKEN
)

위 코드를 통해 모델을 업로드할 때, 권한을 위한 토큰값이 필요합니다.

Huggingface 홈페이지에 로그인한 후 오른쪽 상단 프로필 > Settings > Access Tokens을 누르거나 로그인 후에 링크를 클릭하면 바로 토큰을 관리할 수 있는 페이지가 나타납니다. 페이지 상단을 보면 User Access Tokens부분이 있는데, 여기서 New token을 눌러 토큰을 생성할 수 있습니다. 이 때, Role을 read로 하면 권한 에러가 발생하므로 write로 설정해야합니다.

New Token 클릭
Role을 write로 해서 토큰을 생성
생성된 토큰의 모습

이렇게 생성된 토큰값을 AUTH_TOKEN의 값으로 이용하면 됩니다.

2. CLI 이용

다음은 CLI환경에서 모델을 업로드하는 방법입니다. 우선 아래 코드를 이용해 모델과 토크나이저가 model 디렉토리에 저장되어 있다고 하겠습니다.

from transformers import AutoTokenizer, AutoModelForCausalLM

print("model loading...")

# Model & Tokenizer loading
tokenizer = AutoTokenizer.from_pretrained("sshleifer/tiny-gpt2")
model = AutoModelForCausalLM.from_pretrained("sshleifer/tiny-gpt2")

tokenizer.save_pretrained("./model")
model.save_pretrained("./model")

이후 이용법은 깃허브에 파일을 올리는 것과 동일합니다.

다만 모델의 용량이 큰 것은 수십GB에 이르기 때문에 용량이 큰 파일도 업로드 할 수 있도록 git lfs(Large File Storage)를 설치합니다. 설치가 완료되면 user.email과 user.name을 huggingface 계정의 이메일과 이름으로 설정해줍니다.

git lfs install
git config --global user.email YOUR_EMAIL
git config --global user.name YOUR_NAME

그리고 huggingface-cli login을 입력하여 로그인을 합니다. 로그인을 위해 토큰값이 필요한데, 위의 과정을 통해 생성된 토큰값을 입력하면 됩니다.

huggingface-cli login

이후 아래 과정을 통해 모델을 업로드하면 됩니다.

# Repository 생성
# organization에 생성할 경우
# huggingface-cli repo create YOUR_MODEL_NAME --organization YOUR_ORG_NAME
huggingface-cli repo create YOUR_MODEL_NAME

# 원격 저장소 가져오기
git clone <https://huggingface.co/USER_NAME/YOUR_MODEL_NAME>

# 모델, 토크나이저 Copy
cp -r model/* YOUR_MODEL_NAME
cd YOUR_MODEL_NAME
git add *
git commit -m "Initial commit"
git push

3. huggingface-hub 이용

마지막으로 transformers 패키지를 설치하면 같이 설치되는 huggingface-hub 패키지를 이용하는 방법이 있는데, 이를 이용하면 코드 상에서 모델 업로드 뿐만 아니라 더 많은 작업을 수행할 수 있습니다. 주의할 점은 ipynb파일에서만 작업이 가능한 점입니다.

먼저 아래 코드로 huggingface 계정과 연동시킬 수 있는데, 위의 과정과 마찬가지로 토큰값이 필요합니다.

from huggingface_hub import notebook_login

notebook_login()

create_repo()와 delete_repo()를 이용하면 원격 저장소를 생성하고, 삭제할 수 있습니다.

from huggingface_hub import create_repo

# Repository 생성
create_repo(name=REPO_NAME)

# Repository 삭제
delete_repo(name=REPO_NAME

아래 코드를 이용하면 Repository 객체를 생성할 수 있습니다. 여기서 첫 번째 인자는 생성할 로컬 저장소 이름이고, 두 번째 인자는 연동할 원격 저장소 이름입니다. 코드를 실행하면 객체가 생성되면서 원격 저장소의 파일들이 로컬 저장소에 복사되고, 객체를 이용해서 원격 저장소와 관련된 작업들을 할 수 있습니다.

from huggingface_hub import Repository

repo = Repository(LOCAL_DIR_NAME, clone_from=NAMESPACE/REPO_NAME)

로컬에 저장한 모델을 로컬 저장소로 복사합니다.

!cp -r model/* LOCAL_DIR_NAME

아래 코드를 이용해서 로컬 저장소에 있는 모델들을 원격 저장소에 업로드 할 수 있습니다.

repo.git_add()
repo.git_commit('Initial commit')
repo.git_push()

그리고 upload_file()를 이용하면 add, commit, push 작업 없이 바로 하나의 파일을 업로드할 수도 있습니다. 여기서는 굳이 로컬 저장소에 있지 않은 파일도 업로드가 가능합니다. path_in_repo 인자값은 원격 저장소에 저장되는 파일의 이름입니다.

from huggingface_hub import upload_file, delete_file

# 파일을 직접 업로드
upload_file(
    LOCAL_FILE_PATH,
    path_in_repo=REPO_FILE_PATH,
    repo_id=NAMESPACE/REPO_NAME,
)

delete_file()를 이용하면 원격 저장소의 파일을 바로 삭제할 수도 있습니다.

# 파일 삭제
delete_file(
    REPO_FILE_PATH,
    repo_id=NAMESPACE/REPO_NAME,
)

Reference

파이썬의 딕셔너리를 다루다 보면 정렬을 해야하는 경우가 발생한다. 딕셔너리는 key와 value를 가지고 있으므로 key값을 기준으로 정렬, value값을 기준으로 정렬을 할 수 있다.

우선 기본적으로 리스트와 달리 sort()를 사용할 수 없고 sorted()만 사용할 수 있다.

 

① key값을 기준으로 정렬

dict1 = {'g': 1, 'b': 5, 'z': 8, 'e': 2, 'k': 10}


sorted_dict1 = sorted(dict1.items(), key=lambda x: x[0])

print(sorted_dict1) # [('b', 5), ('e', 2), ('g', 1), ('k', 10), ('z', 8)]
print(type(sorted_dict1)) # <class 'list'> ==> dict타입 아님 주의!!


sorted_dict1 = dict(sorted(dict1.items(), key=lambda x: x[0])) 

print(sorted_dict1) # {'b': 5, 'e': 2, 'g': 1, 'k': 10, 'z': 8}
print(type(sorted_dict1)) # <class 'dict'>

위의 코드와 같이 sorted()결과는 리스트로, 딕셔너리로 사용하고 싶으면 dict()를 이용해야 한다.

그리고 정렬된 key값만 얻고싶은 경우에는 key인자를 설정하지 않아도 된다.

dict1 = {'g': 1, 'b': 5, 'z': 8, 'e': 2, 'k': 10}


sorted_dict1 = sorted(dict1) 

print(sorted_dict1) # ['b', 'e', 'g', 'k', 'z']
print(type(sorted_dict1)) # <class 'list'>


sorted_dict1 = sorted(dict1.keys()) 

print(sorted_dict1) # ['b', 'e', 'g', 'k', 'z']
print(type(sorted_dict1)) # <class 'list'>

sorted()에 dict1로 하는 경우와 dict1.keys()로 한 경우가 동일한 결과를 갖는다. 결과 type은 리스트다.

 

② value값을 기준으로 정렬

dict1 = {'g': 1, 'b': 5, 'z': 8, 'e': 2, 'k': 10}


sorted_dict1 = sorted(dict1.items(), key=lambda x: x[1]) 

print(sorted_dict1) # [('g', 1), ('e', 2), ('b', 5), ('z', 8), ('k', 10)]
print(type(sorted_dict1)) # <class 'list'> ==> dict타입 아님 주의!!


sorted_dict1 = dict(sorted(dict1.items(), key=lambda x: x[1]))

print(sorted_dict1) # {'g': 1, 'e': 2, 'b': 5, 'z': 8, 'k': 10}
print(type(sorted_dict1)) # <class 'dict'>

마찬가지로 sorted()결과는 리스트로, 딕셔너리로 사용하고 싶으면 dict()를 이용해야 한다.

그리고 정렬된 value값만 얻고싶은 경우에는 간단하게 values()를 이용하면 된다.

dict1 = {'g': 1, 'b': 5, 'z': 8, 'e': 2, 'k': 10}


sorted_dict1 = sorted(dict1.values()) 

print(sorted_dict1) # [1, 2, 5, 8, 10]
print(type(sorted_dict1)) # <class 'list'>

 

'개발 > Python' 카테고리의 다른 글

[Python] sort()의 key를 이용한 리스트 다중 정렬  (0) 2020.12.28

백준 1931번 문제를 해결하면서 리스트를 다중 정렬이 필요했다.

 

그러면 다중 정렬이란 무엇일까?? 다음과 같은 리스트가 있다고 하자.

list1 = [(5, 4), (4, 4), (4, 3), (5, 2), (3, 1), (3, 2), (2, 5), (2, 1)]

원소가 투플인 해당 리스트를 다음과 같은 조건으로 정렬하고자 한다.

투플의 첫 번째 원소에 대해서 오름차순

② 첫 번째 원소의 값이 같으면 두 번째 원소에 대해서 오름차순

 

먼저 sort()의 key에서 투플의 첫 번째 원소만을 조건으로 주면 결과는 다음과 같다.

list1.sort(key=lambda x: x[0])
# [(2, 5), (2, 1), (3, 1), (3, 2), (4, 4), (4, 3), (5, 4), (5, 2)]

첫 번째 원소에 대해서 정렬은 되지만, 두 번째 원소에 대해서는 정렬이 되지 않고 입력한 순서대로 출력이 된다.

 

따라서 조건에 맞게 정렬하기 위해서는 다음과 같이 작성하면 된다.

list1.sort(key=lambda x: (x[0], x[1]))
# [(2, 1), (2, 5), (3, 1), (3, 2), (4, 3), (4, 4), (5, 2), (5, 4)]

lambda함수에서 정렬 조건에 맞에 투플 형식으로 주면 된다. 우선순위가 높은 조건을 먼저 인자로 준다.

 

추가적인 정렬예제들

 투플의 첫 번째 원소에 대해서 오름차순

② 첫 번째 원소의 값이 같으면 두 번째 원소에 대해서 내림차순

list1.sort(key=lambda x: (x[0], -x[1]))
# [(2, 5), (2, 1), (3, 2), (3, 1), (4, 4), (4, 3), (5, 4), (5, 2)]

 

 투플의 두 번째 원소에 대해서 오름차순

번째 원소의 값이 같으면 첫 번째 원소에 대해서 오름차순

list1.sort(key=lambda x: (x[1], x[0]))
# [(2, 1), (3, 1), (3, 2), (5, 2), (4, 3), (4, 4), (5, 4), (2, 5)]

 

'개발 > Python' 카테고리의 다른 글

[Python] 딕셔너리(Dict) 정렬하기  (0) 2021.01.27

+ Recent posts