1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CUDA入門:GPU計算と並列コンピューティングの基礎知識

Posted at

はじめに

現代のAI、ビッグデータ解析、科学技術計算などでは高速な並列処理が不可欠であり、その鍵を握る技術の一つがNVIDIA社の「CUDA」です​。CUDA(Compute Unified Device Architecture)は、GPUの計算能力を汎用の計算に活用するためにNVIDIAが2006年に開発・公開した並列コンピューティングプラットフォームです​。登場以前はGPUは主にグラフィックス描画専用でしたが、CUDAによりGPUを汎用計算に利用するGPGPU(General-Purpose computing on GPUs)の時代が開き、科学技術計算や機械学習など多くの分野で計算処理の大幅な高速化が実現しました​。

この記事では、CUDAとは何かその仕組みとCPUとの違いCUDAプログラミングの基本(C++を中心に、Pythonユーザー向けのPyCUDAやCuPyにも触れます)、そしてディープラーニングや画像処理、数値シミュレーションといった実用例と利点について解説します。また、CUDAを学ぶステップ初心者がつまずきやすいポイントと対処法学習リソースの紹介、さらにはOpenCLなど他の並列計算プラットフォームとの比較にも簡単に触れます。Pythonユーザーの視点も交え、できるだけ丁寧に体系的に説明していきます。

CUDAとは何か?CPUとGPUの違い

CUDAはNVIDIA製GPUのための並列計算プラットフォーム兼プログラミングモデルです。もともとGPU(Graphics Processing Unit)は名前の通りグラフィックス処理のために設計されたプロセッサですが、CUDAの登場以降は画像処理以外の汎用計算にも広く利用されるようになりました​。CUDAを使うことで、開発者はGPU上で動作する並列なプログラム(カーネル)を書き、従来CPUでは非常に時間のかかった計算を大幅に高速化できます。

では、GPUを使うと何故速いのか、その背景としてCPUとGPUのアーキテクチャ(設計思想)の違いを見てみましょう。

CPUとGPUのアーキテクチャの違い

CPUは高性能なコアを数個~数十個程度搭載し、各コアが高いクロックで複雑な処理を順次(シリアルに)実行するのに優れています​。例えば一般的なPC用CPUは4~16個程度のコアを持ち、分岐や多様な命令を巧みにさばきながらOSや日常アプリの処理を担います。
一方GPUは数百~数千個ものシンプルな演算コアを内部に備えており、同じような計算を大量のデータに対して並列に実行することに特化しています​。つまり「一つの作業を素早くこなす」CPUに対し、GPUは「大量の作業を同時にこなす」ことを得意とするのです​。またGPUは総じてCPUよりもメモリ帯域幅が高く、膨大なデータを並列処理するために最適化されています​。

GPU vs CPU

並列処理に特化したGPU

GPUが並列計算で高速な理由は大きく3点あります。

  1. コア数が非常に多いこと
    最新のGPUには数千ものコアが搭載され、データを細分化して同時処理できます​。

  2. 高いスループット志向
    GPUは一度に大量の演算を進めるよう設計されており、総合的な処理効率(スループット)が高くなっています​。

  3. シンプルなコア設計
    各GPUコアはCPUコアほど複雑ではなく、機能を絞る代わりに動作を並列に最適化しています​。

これらにより、例えばGPUは同じ演算を数千のデータ要素に一斉に適用する処理で圧倒的な性能を発揮します。
実際、理論演算性能を比べると、最新のデータセンター向けGPU(例:NVIDIA A100では倍精度で約9.7 TFLOPS)は同世代のCPU(例:12コアのXeonで約1.3 TFLOPS)に対して数倍以上の演算性能を持っています​。もちろん個々のGPUコアはCPUコアより遅いのですが、「質より量」で圧倒的な並列計算を実現するのがGPUの強みです​。
一方CPUは少ないコアで高速クロック動作し、分岐の多い処理や逐次処理が必要なタスクを効率よく処理します​。

このようにCPUとGPUは設計思想が異なり、それぞれ得意分野が異なります。CUDAはこのGPUの並列処理能力を汎用計算に活用するための仕組みと言えます。

CUDAプログラミングの全体像(C++とPython)

それではCUDAを使ってGPUで計算するには具体的にどうプログラムを記述し、実行するのでしょうか。基本となるのはC/C++言語を用いたCUDAプログラミングですが、Pythonユーザー(私)向けには後述するようにPyCUDACuPyNumbaなどのツールを使ってGPU計算を行う方法もあります。

本節ではまずCUDA C++によるプログラミングモデルと開発フローの全体像を説明し、その後でPythonからの利用方法に触れます。

CUDAのプログラミングモデル概要

CUDAではGPU上で動作する関数をカーネル関数kernel)と呼びます。カーネル関数はGPU上の多数のスレッド並列で実行され、並列処理を記述する核となるものです。GPU上のスレッドは階層構造を取り、スレッドが集まったものをブロック、ブロックの集まりをグリッドと呼びます​。
開発者はカーネル関数を定義し、実行時に「グリッドとブロックの数(次元)」を指定してGPU上に大量のスレッドを生成・実行させます。各スレッドはカーネル内で自分のインデックス(threadIdxやblockIdxなどで取得)に応じたデータ要素を処理することで、大規模なデータ並列計算が実現できます。
例えば1次元配列同士の加算であれば、配列の各要素を処理するカーネルを用意し、配列長Nに対応してN個のスレッドを起動することで全要素の加算を並列に行う、といった具合です。 スレッドはブロック単位でまとまり(ブロック内のスレッドは協調して動作し高速な共有メモリを共有できます)、各ブロックはGPU上の演算ユニット(SM: Streaming Multiprocessor)上で並行実行されます。

開発者は問題に応じて「1ブロックあたり何スレッド × いくつのブロック」という構成(グリッドサイズとブロックサイズ)を決めてカーネルを起動します。なお、GPUにはCPUとは独立したデバイスメモリVRAM)がある点も重要です。

基本的なCUDAプログラムでは、CPU側(ホスト)のメモリからデバイス(GPU)メモリにデータを転送し、GPUでカーネルを実行した後、結果をまたホストに送り返すという手順を踏みます。こうしたメモリ転送にはコストがかかるため、GPU計算の利点を最大化するには転送すべきデータ量やタイミングを工夫する必要があります。

GPU vs CPU

CUDAプログラム開発の流れ

実際のCUDA C/C++プログラムは、基本的に「ホスト(CPU側)でGPUを制御し、GPU上で並列カーネルを実行する」形で構成されます。以下に典型的なCUDAプログラムの流れを示します​。

  1. GPUメモリの確保
    cudaMalloc関数などを使い、GPUデバイス上に必要なメモリ領域を確保します(CPUのmallocに相当)。GPUメモリはCPUメモリとは別物なので、まず使用領域を確保する必要があります。
  2. データ転送(ホスト→デバイス)
    cudaMemcpy等でCPU側のデータをGPUメモリにコピーします​。これにより計算に必要な入力データがGPU側に渡されます。
  3. カーネルの起動
    実行するカーネル関数とグリッド・ブロックサイズを指定してカーネルを起動します(例: kernel<<>>(arguments...) のような構文)。指定した数のスレッドがGPU上で生成され並列計算を開始します。
  4. 実行の同期
    必要に応じてcudaDeviceSynchronizeなどでGPU上の計算完了を待ちます。非同期実行のままにしたい場合以外は、通常このタイミングでGPUのカーネル実行終了を待ちます(この間CPUは別の作業をすることも可能です)。
  5. データ転送(デバイス→ホスト)
    GPUでの計算結果を再びcudaMemcpyでCPU側にコピーします​。これで結果データをホストプログラムから利用できるようになります。
  6. GPUメモリの解放
    使用が終わったGPUメモリをcudaFreeで解放します​。以上で一連の計算サイクルが完了です。

以上が基本的な流れで、実際にはエラー処理(各CUDA呼び出し後のエラーコード確認)やストリームを使った非同期実行、高度なメモリ操作(共有メモリやピン留めメモリの利用)などを組み合わせて最適化を行います。
しかし初心者のうちは、まずこの「メモリ確保→データ転送→カーネル実行→結果回収」というサイクルを確実に押さえることが大切です。

以下に、この流れを実現する簡単なCUDA C++コードの例を示します。ベクトルの要素ごとの加算を行うプログラムで、CUDAカーネルとホスト側処理の一部を抜粋しています。

c++
#include <cuda_runtime.h>
#include <iostream>

// GPU上で実行されるカーネル関数(ベクトル加算)
__global__ void addArrays(const float* A, const float* B, float* C, int N) {
    // 各スレッドが処理する配列インデックスを計算
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < N) {
        C[i] = A[i] + B[i];  // 対応するAとBの要素を加算してCに書き込む
    }
}

int main() {
    int N = 1000000;
    size_t bytes = N * sizeof(float);
    float *h_A, *h_B, *h_C;
    
    // ホスト側メモリ確保と初期化
    h_A = (float*)malloc(bytes); h_B = (float*)malloc(bytes); h_C = (float*)malloc(bytes);
    for(int i=0; i<N; ++i){ h_A[i]=1.0f; h_B[i]=2.0f; }
    
    // デバイス側メモリ確保
    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, bytes); 
    cudaMalloc(&d_B, bytes);
    cudaMalloc(&d_C, bytes);
    
    // ホスト→デバイス転送
    cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);
    
    // 256スレッド×適切なブロック数でカーネル起動
    int blockSize = 256;
    int numBlocks = (N + blockSize - 1) / blockSize;
    addArrays<<<numBlocks, blockSize>>>(d_A, d_B, d_C, N);
    
    // GPUでの計算完了を待機
    cudaDeviceSynchronize();
    
    // デバイス→ホスト転送
    cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);
    
    // 結果の検算
    std::cout << "C[0] = " << h_C[0] << std::endl;  // 3.0 (1.0+2.0) のはず

    // デバイスメモリ開放と後処理省略
    return 0;
}

上記の例では、addArraysというカーネルを定義し(__global__キーワードで修飾された関数)、<<>>というCUDA独自の構文でそれを起動しています。threadIdx.xblockIdx.xといった組み込み変数を用いて各スレッドが処理すべき配列インデックスを計算し、それぞれ対応する要素同士の加算を実行します。ホスト側main関数ではCUDAのAPIを使ってGPUメモリを確保・転送し、結果を受け取るまでの流れを実装しています。
PythonからCUDAを使う(PyCUDA, CuPy, Numbaなど)

CUDAプログラミング自体はC/C++で行うのが基本ですが、PythonユーザーでもGPUを活用する方法があります。低レベルにはPyCUDAというライブラリがあり、これはPythonからCUDAカーネルを呼び出すための仕組みを提供します。PyCUDAではCUDA CのカーネルコードをPython文字列として記述し、ランタイムにコンパイルして実行することができます。
また、もう少し高レベルな選択肢としてCuPyがあります。CuPyはNumPyとほぼ同じ感覚で使える配列演算ライブラリで、内部的にCUDAを用いて演算を高速化しています​。
たとえば、NumPyで書いていたコードをCuPyに置き換えるだけで裏でCUDAが動いて高速化されるため、ユーザーはGPU特有のメモリ管理やカーネル記述を意識せずにGPU計算を利用できます​。

以下にCuPyを用いた簡単な例を示します。先ほどと同じベクトル加算を、Python+CuPyで行った場合です。

python
import cupy as cp

# サイズNの配列をGPU上に作成(0,1,2,...,N-1)
N = 1000000
a = cp.arange(N, dtype=cp.float32)
b = cp.arange(N, dtype=cp.float32) * 2  # bは0,2,4,...とする

# GPU上でベクトル加算(ブロードキャストや要素毎加算もNumPy同様に書ける)
c = a + b

# 計算結果をホスト側(NumPy配列)に取り出して確認
result = cp.asnumpy(c)

print(result[0])  # 0+0*2 = 0,  result[1] = 3, ... と期待通りか確認

上記のように、CuPyを使えばほとんどNumPyと変わらないコードでGPU計算を実行できます。裏ではCUDA用のカーネルが最適化されて動いており、大規模なデータ処理でもCPUに比べ高速に動作します。また、PythonからGPUを使う方法としては他にもNumbaがあります。NumbaはPythonコード中にCUDAカーネルを記述するデコレーター(@cuda.jit)を提供し、Python内でGPUカーネルを定義・コンパイルして実行できます。PyTorchやTensorFlowなどのディープラーニングフレームワークも、ユーザーがPythonで記述したモデルやテンソル演算を内部的にCUDAで実行しています。つまりPythonユーザーであっても、CUDAの概念を理解すれば自分でCUDAカーネルを書く場合だけでなく、高レベルライブラリを効率よく使う上でもメリットがあります。

CUDAの実用例と活用分野

CUDAは様々な分野の計算処理を高速化し、大きな成果を上げています。ここでは代表的な例として、ディープラーニング、画像処理、数値シミュレーションの3つの分野におけるCUDAの活用方法と利点を紹介します。

ディープラーニングにおけるCUDA

近年のディープラーニングブームにおいて、CUDA対応GPUの存在は不可欠でした。ニューラルネットワークの学習(トレーニング)は大量の行列演算やベクトル演算を反復する計算で構成されていますが、GPUはこれらを大規模並列で処理できるためCPUに比べ桁違いに高速です。実際、多くのディープラーニングフレームワーク(TensorFlow、PyTorchなど)はCUDA上で動作するcuDNNやcuBLASといったNVIDIAの高速ライブラリを内部で利用し、GPUの性能を引き出しています。
例えば畳み込みニューラルネットワークの訓練では、画像に対する畳み込み演算をGPUで一括並列実行することで、1枚あたりの処理をCPUの何十倍も速く行えます。CUDA対応GPUがなければ、現在のような大規模モデルの学習は現実的な時間では困難でしょう。研究から実務まで、モデルの学習や推論にはCUDAによるGPU加速が当たり前に使われており、GPUあたり数倍~数十倍の速度向上が報告されています。

特に画像認識自然言語処理の巨大モデルのトレーニングでは、複数GPUを使った並列学習(マルチGPU、分散学習)も行われ、CUDAはそれらを支える基盤技術となっています。 Pythonユーザーの視点では、ディープラーニングでCUDAを直接意識する機会は少ないかもしれません。
例えばPyTorchではtensor.to('cuda')と指定するだけでデータをGPUに載せ、以降の計算は自動的にCUDA実行されます。しかしその裏ではCUDAカーネルが動いているため、CUDAの知識があると計算ボトルネックの理解やGPUメモリの使われ方などを把握しやすくなります。
またモデルの最適化やカスタムレイヤーの実装で低レベルに踏み込む際、CUDA C++で独自カーネルを書くケースもあります。総じて、ディープラーニング分野ではCUDAによるGPU計算は高速化の要であり、大規模データ処理を可能にする原動力です。

画像処理におけるCUDA

画像や映像の処理もCUDAの得意分野です。元々GPUはグラフィックス処理用に設計されているため、画像処理に関わる計算との親和性が高いと言えます。

例えば画像フィルタ(畳み込みフィルタやエッジ検出など)、画像の変形処理、動画のエンコード/デコード、コンピュータビジョンアルゴリズム(物体検出や画像分類)など、画素単位・ピクセル単位で独立した処理が多数あるタスクはGPUで大幅に高速化できます。CUDAを用いれば、1ピクセルの処理を1スレッドが担当し、何百万ものスレッドで画像全体を並列に処理するといった実装が可能です。実際、OpenCVにはCUDA対応のモジュール(opencv_cudaモジュール)があり、多くの画像処理基本機能がGPUで動作します。
これを使うと、たとえば高解像度画像のフィルタ処理がリアルタイムに近い速度で実行できたり、動画ストリームに対するリアルタイム物体検出がより高FPSで可能になったりします。

具体例として、医用画像の解析ではCUDAによるGPU並列化で処理時間が劇的に短縮されたケースがあります。また映像編集ソフトやエンコードソフトでも、CUDA対応GPUを使うことでレンダリング時間を大幅短縮できる機能が提供されています。画像処理分野は演算パターンが行列演算や配列操作に落とし込みやすく、CUDA用に高度に最適化されたライブラリ(NVIDIA Performance Primitives (NPP) など)も存在します。Pythonユーザーであれば、先述のCuPyを使って自前の画像処理コードをGPU化したり、TensorFlow/PyTorchベースで画像処理アルゴリズムを組んでGPU実行する、といった方法が考えられます。

総じて、画像処理では「同じ処理を全画素に適用する」ような並列タスクが多く、CUDAの並列計算モデルと非常にマッチするため大きな性能向上が得られます。

数値シミュレーションにおけるCUDA

科学技術計算や物理シミュレーションの分野でも、CUDAは計算速度の飛躍的向上に貢献しています。例えば流体力学のシミュレーション、気象予測モデル、分子動力学、金融工学のモンテカルロシミュレーション、CGにおける物理ベースレンダリングなど、膨大な計算量を要するシミュレーションは従来スーパーコンピュータで実行されてきました。現在ではこうした計算にもGPUが用いられ、単体のGPUで数十~百個規模のCPUクラスターに匹敵する性能を発揮する場合もあります。
実際、ある研究コードをGPU対応に書き換えた事例では、CPUのみの場合と比べて約10倍の速度向上を達成したと報告されています。また理論上はGPUの性能はCPUの数十倍に及ぶため、問題によってはそれに近いスピードアップが得られることもあります​。

数値シミュレーション系のソフトウェアでは、近年はCUDA対応が一種のトレンドになっています。例えば科学技術計算ライブラリの分野では、CUDA対応の線形代数ライブラリ(cuBLASMAGMAなど)やFFTライブラリ(cuFFT)を組み込むことでGPU加速を図っています。Python環境でも、並列行列演算を扱うNumPy/SciPyコードをCuPyに置き換えてシミュレーションを回す、TensorFlowで微分方程式を解く、などGPUを活用する動きがあります。特に機械学習以外のHPC分野でもPython+GPUという組み合わせが注目されており、「従来はFortran/Cで書いていたシミュレーションをより手軽なPythonで書き、裏でGPUをフル活用する」というスタイルも増えてきています​。

こうした流れを支えているのもCUDAをはじめとするGPU計算基盤です。総じて、数値シミュレーション分野ではCUDAを用いることで計算時間を桁違いに短縮し、より高解像度・高精度なシミュレーション多数パラメータの同時実験が可能になるという大きなメリットがあります。

初心者がつまずきやすいポイントと対処法

  • 環境構築のトラブル
    正しくCUDAがインストールできていないとコンパイルエラー(例: nvcc: command not found や cuda_runtime.h: No such file or directory)が発生します​。この場合、パスが通っているかドライバのバージョンは適切かなどを確認してください。NVIDIAの公式サイトやフォーラムには同様のトラブルシューティング情報があります。

  • メモリ関連のバグ
    GPUメモリは手動で管理する必要があり、領域不足やコピー忘れ、cudaFree忘れによるメモリリークなどが起こりがちです。特にホストとデバイス間で配列サイズや型が一致しているかコピー方向(HostToDevice/DeviceToHost)が正しいかを注意深く確認しましょう。またカーネル内での配列アクセスが範囲外になっていないか(これが原因でCUDA error: device-side assert triggered等のエラーになることがあります)もデバッグ時に疑ってください。エラー時にはcudaGetLastError()で直前のエラーをチェックすると原因追及の助けになります。

  • 並列プログラム特有の難しさ
    CUDAカーネルは多数のスレッドで並行動作するため、場合によってはスレッド間の競合や同期問題が発生します。例えば複数のスレッドが同じ変数を同時に書き込むと競合が起きます(これを防ぐためにアトミック演算同期バリアを使う)。初心者の段階では極力こうしたデータ競合のない問題設定(各スレッドが独立に処理できる状況)を選ぶと良いでしょう。どうしても必要な場合はCUDAのatomic操作 (atomicAdd等) を調べて用います。また条件分岐がカーネル内に多いとWarpの分岐による性能低下が起こることがありますが、これも最初は深く気にせず「まず正しく動く並列プログラムを書く」ことを優先してください。

  • デバッグが難しい
    GPU上で動くコードのデバッグは容易ではありません。標準のデバッガでは追えないので、cuda-gdbやNsight ComputeなどNVIDIA提供のデバッグ/プロファイルツールを使う必要があります。初心者にはハードルが高いですが、代替として段階的に検証する手法があります。例えばカーネルを極端に小さい入力サイズで動かして結果をCPU計算と突き合わせてみる、途中結果を一部ホストにコピーしてチェックするなどです。最適化よりまず正しい実装を目指し、徐々に性能チューニングしていくと良いでしょう。

  • ドキュメントやエラーメッセージの読み飛ばし
    CUDAのエラーメッセージや警告は英語ですが、内容をよく読むことが大事です。「Unknown error」等わかりにくいものもありますが、公式ドキュメントやNVIDIAのデベロッパーフォーラムには同様の質問が多数あります。同じ失敗を繰り返さないためにも公式資料を参照する習慣をつけましょう。加えて、まずはシンプルなコードから書いて徐々に拡張することで、どの段階でバグが入り込んだかを特定しやすくなります。

以上のようなポイントに注意しつつ進めれば、初心者でも段階的にCUDAプログラミングに習熟できるでしょう​。困ったときは公式のサンプルやドキュメントに立ち返ったり、インターネット上のコミュニティ(Stack OverflowやQiita、NVIDIAフォーラム)で情報収集すると解決のヒントが得られることが多いです。

他の並列計算プラットフォームとの比較(OpenCLなど)

最後に、CUDAとよく比較される他の並列計算プラットフォームについて簡単に触れておきます。代表的なものにOpenCLがありますが、これはKhronos Groupが策定したオープンな並列計算規格で、NVIDIA GPUに限らずAMDやIntelのGPU、CPUなど様々なハードウェアで動作する汎用性が特徴です。CUDAがNVIDIA製GPU専用でそのハードウェアを最大性能で活用できるのに対し、OpenCLはハードウェア非依存で幅広いデバイスで動作するという違いがあります。
例えば、プロジェクトの対象ハードウェアがNVIDIA GPUに限定され高い性能が求められるならCUDAが有力候補ですが、AMD GPUや統合GPUなど異種プラットフォームでも動かす必要があるならOpenCLを選ぶことになるでしょう。

もっとも、2020年代現在では深層学習や科学計算の分野ではCUDAエコシステムのほうが充実しており、主流の機械学習フレームワークの多くはCUDA中心に最適化されています​。OpenCL対応の機械学習ライブラリも存在しますが数は限られ、最新の研究でもCUDAの存在感が依然として大きいのが現状です​。
一方でOpenCLはクロスプラットフォームゆえに組み込み機器モバイル、さらにはCPUとGPUの混在環境での並列計算など幅広い用途で使われています​。したがって選択の基準は、「特定ベンダー(NVIDIA)のGPUで最高性能を引き出す必要があるか」「複数のデバイス間の移植性を重視するか」といった点になります。 なお他にも、GPUを使った並列計算プラットフォームとしてはpenACC(コンパイラ指示文ベースでGPU並列化を行う仕組み)や、MicrosoftのDirectCompute(Windows環境でGPU計算を行うAPI、DirectXの一部)などがあります​。
OpenACCは既存のC/C++/Fortranコードにプラグマを書くだけでGPUを含む並列化をコンパイラ任せにできるもので、主にHPC分野で利用されています。DirectComputeはゲーム開発向けですが、Windows限定です。一方OpenCLはプラットフォームに依存しない汎用性から、科学技術計算でも広く採用されています​。

総じて、「とにかくNVIDIA GPUで速くしたい」場合はCUDA、 「色々なデバイスで動かしたい」場合はOpenCL(やそれに対応したフレームワーク)を選ぶといった使い分けがなされています。


以上、CUDAの基礎からプログラミング方法、活用分野、学習法、他プラットフォーム比較までを概観しました。CUDAは初心者にとって最初は取っつきにくい部分もありますが、扱えるようになると飛躍的な性能向上を得られる強力な技術です。Pythonユーザーであっても、その仕組みを理解しておけば高レベルライブラリを使う際にも計算の裏側を理解しやすくなるでしょう。

ぜひ公式リソースや本稿で紹介した教材を活用しながら、少しずつGPU並列プログラミングに挑戦してみてください。高い並列計算能力を活かせるCUDAスキルは、ディープラーニングから科学計算まで幅広い分野で強力な武器になるはずです。​

参考文献

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?