0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenCL

0
Posted at

参考文献:Interface 2022年8月号 第5部第2章 OpenCL を試す
学習記録です。

環境

ノート PC

  • HP Pavilion Laptop 15-eh3xxx
  • AMD Ryzen 5 7530U with Radeon Graphics

OpenCL の導入

NVIDIA 製の GPU でしか動かない CUDA とは違い、OpenCl はメーカーや設計が異なるハードウェアを同じコードで効率よく動かすことができます。

OpenCL の SDK(Software Development Kit:ソフトウェア開発キット)をインストールします。
OpenCL は共通規格ですが、実際に計算を行うのは NVIDIA や AMD、Intelの チップです。そのため、各メーカーが自社のチップを効率よく動かせるように、専用の SDK を配布しています。また、最近では、Khronos OpenCL SDK という、メーカーを問わず使える公式の標準SDKがGitHubなどで公開されています。

AMD が独自の OpenCL SDK の単体配布を終了した?という記事が出てきたので、Khronos 公式の SDK をインストールしました。
Visual Studio が Khronos 公式のヘッダーとライブラリを認識するようにします。

git clone https://github.com/microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat

.\vcpkg\vcpkg install opencl:x64-windows

.\vcpkg\vcpkg integrate install

認識するようにしたのですが、なぜかパスが通っていなかったので、手動で設定しました。プロジェクトのプロパティ > 追加のインクルードディレクトリ にパスを設定します。

"(vcpkg を置いた場所)\vcpkg\installed\x64-windows\include"

ライブラリもパスを通します。(リンカー > 全般)

"(vcpkg を置いた場所)\vcpkg\installed\x64-windows\lib"

ノート PC の内蔵 GPU

インテルや AMD の CPU を搭載しているノート PC には内蔵 GPU も搭載されており、内蔵 GPU は iGPU(Integrated GPU)と呼ばれます。

CPU と iGPU の情報を取得する

テキストで紹介されていたプログラムにメモリ解放部分がない?ので追加しました。

#include <vector>
#include <stdio.h> // printfのために追加

// OpenCLのヘッダ・ファイル
#include <CL/cl.h>
#include <CL/cl_platform.h>

// OpenCLライブラリ
#pragma comment(lib, "OpenCL.lib")

int main()
{
    cl_uint num_of_platforms = 0;

    // プラットフォーム数を取得
    cl_int err = clGetPlatformIDs(0, 0, &num_of_platforms);

    std::vector<cl_platform_id> platforms(num_of_platforms);

    // プラットフォーム識別子を取得
    err = clGetPlatformIDs(num_of_platforms, &platforms[0], 0);

    // 各プラットフォームごとに(通常は1つ)
    for (cl_uint i = 0; i < num_of_platforms; i++)
    {
        cl_uint num_of_devices = 0;

        // OpenCL対応デバイス数を取得
        err = clGetDeviceIDs(
            platforms[i],
            CL_DEVICE_TYPE_GPU | CL_DEVICE_TYPE_CPU, // CPUとGPUを取得
            0,
            0,
            &num_of_devices
        );

        cl_device_id* id = new cl_device_id[num_of_devices];

        // デバイス識別子を取得
        err = clGetDeviceIDs(
            platforms[i],
            CL_DEVICE_TYPE_GPU | CL_DEVICE_TYPE_CPU, // CPUとGPUを取得
            num_of_devices,
            id,
            0
        );

        // 各デバイスについて
        for (cl_uint j = 0; j < num_of_devices; j++)
        {
            size_t length;
            cl_uint freq, units;
            char deviceName[128];
            cl_device_svm_capabilities svmCapability;

            // デバイス情報を取得
            err = clGetDeviceInfo(id[j],
                                  CL_DEVICE_MAX_CLOCK_FREQUENCY,
                                  sizeof(cl_uint), &freq, &length);
            err = clGetDeviceInfo(id[j],
                                  CL_DEVICE_MAX_COMPUTE_UNITS,
                                  sizeof(cl_uint), &units, &length);
            err = clGetDeviceInfo(id[j], CL_DEVICE_NAME,
                                  sizeof(deviceName), deviceName, NULL);
            // 変数名を svmCapability に統一
            err = clGetDeviceInfo(id[j],
                                  CL_DEVICE_SVM_CAPABILITIES,
                                  sizeof(svmCapability), &svmCapability, NULL);

            if (svmCapability & CL_DEVICE_SVM_COARSE_GRAIN_BUFFER)
            {
                printf("%s %d Compute units %dMHz SVM supported\n",
                       deviceName, units, freq);
            }
            else
            {
                printf("%s %d Compute units %dMHz\n",
                       deviceName, units, freq);
            }
        }
        // メモリの解放を追加
        delete[] id;
    }

    return 0;
}

実行結果 ↓

gfx90c 7 Compute units 2000MHz SVM supported

GPU は認識されましたが、CPU が認識されていません。CPU が OpenCL から認識されるためには、OpenCL CPU Runtime という専用のソフトが必要だそうです。

OpenCLを動かすための最も標準的なソフトが Intel CPU Runtime で、AMD の CPU でも問題なく動作するとのことで、ダウンロードページより、ダウンロードしました。

Starting in the 2020 February release ("igfx_win10_100.7870.exe"), the CPU runtime for OpenCL is no longer included with the OpenCL Driver package. とのことです。

再度実行 ↓

gfx90c 7 Compute units 2000MHz SVM supported
AMD Ryzen 5 7530U with Radeon Graphics          12 Compute units 0MHz SVM supported

GPU, CPU 共に情報を取得できました。

「CPU」「OpenMP(CPU並列)」「iGPU」の3パターンで計測・比較する

まず、Visual Studio の OpenMP を有効にします。
プロジェクトのプロパティ > C/C++ > 言語 > OpenMP サポート を 「はい (/openmp)」 に設定しました。

演算用のコードについて、
テキストでは命令キュー作成時に clCreateCommandQueue が使われていますが、この関数は OpenCL 2.0 から「古い形式」とされ現在は clCreateCommandQueueWithProperties が使われているそうです

一度実行した際に GPU の結果しか表示されなかったので、GPUから戻ってきた後、CPUが再び触る前にもう一度 Map を入れました、CPU,
OpenMP の結果も表示されるようになりました。

#include <stdio.h>
#include <vector>
#include <windows.h>
#include <CL/cl.h>

#pragma comment(lib, "OpenCL.lib")

// --- リスト2: 配列同士の足し算をするカーネル ---
const char* kernelSource =
"__kernel void vectorAdd(__global double* a,          "
"                        __global double* b,          "
"                        __global double* c,          "
"                        const unsigned int n)        "
"{                                                    "
"    int id = get_global_id(0);                       "
"    if (id < n)                                      "
"        c[id] = a[id] + b[id];                       "
"}                                                    ";

#define VECTOR_SIZE 50000000

int main()
{
    cl_int err;
    cl_uint num_platforms;
    clGetPlatformIDs(0, NULL, &num_platforms);
    std::vector<cl_platform_id> platforms(num_platforms);
    clGetPlatformIDs(num_platforms, &platforms[0], NULL);

    cl_device_id deviceId;
    // 最初のプラットフォームからGPUを取得
    clGetDeviceIDs(platforms[0], CL_DEVICE_TYPE_GPU, 1, &deviceId, NULL);

    // --- リスト3: カーネルを実行する流れ ---
    // コンテキスト作成
    cl_context context = clCreateContext(NULL, 1, &deviceId, NULL, NULL, &err);
    // 命令キュー作成
    cl_command_queue queue = clCreateCommandQueueWithProperties(context, deviceId, NULL, &err);
    // カーネルコードからプログラムを生成
    cl_program program = clCreateProgramWithSource(context, 1, &kernelSource, NULL, &err);
    // ビルド(SVM使用のためバージョン2.0以上を指定)
    err = clBuildProgram(program, 0, NULL, "-cl-std=CL2.0", NULL, NULL);
    // カーネル生成
    cl_kernel vectorAdd = clCreateKernel(program, "vectorAdd", &err);

    // --- リスト4: 演算対象のデータをデバイス側に渡す (SVM編) ---
    // Shared Virtual Memory を確保
    double* A = (double*)clSVMAlloc(context, CL_MEM_READ_ONLY, sizeof(double) * VECTOR_SIZE, 0);
    double* B = (double*)clSVMAlloc(context, CL_MEM_READ_ONLY, sizeof(double) * VECTOR_SIZE, 0);
    double* C = (double*)clSVMAlloc(context, CL_MEM_WRITE_ONLY, sizeof(double) * VECTOR_SIZE, 0);

    // この領域へホスト側でアクセスする (Map)
    clEnqueueSVMMap(queue, CL_TRUE, CL_MAP_WRITE, A, sizeof(double) * VECTOR_SIZE, 0, NULL, NULL);
    clEnqueueSVMMap(queue, CL_TRUE, CL_MAP_WRITE, B, sizeof(double) * VECTOR_SIZE, 0, NULL, NULL);

    // 入力データを設定
    for (int i = 0; i < VECTOR_SIZE; i++) {
        A[i] = i;
        B[i] = (double)VECTOR_SIZE - i;
    }

    // ホスト側のアクセス終了 (Unmap)
    clEnqueueSVMUnmap(queue, A, 0, NULL, NULL);
    clEnqueueSVMUnmap(queue, B, 0, NULL, NULL);

    unsigned int vectorSize = VECTOR_SIZE;
    size_t globalSize = VECTOR_SIZE;
    size_t localSize = 8;

    // カーネルのパラメータを設定 (SVMポインタを使用)
    err = clSetKernelArgSVMPointer(vectorAdd, 0, A);
    err |= clSetKernelArgSVMPointer(vectorAdd, 1, B);
    err |= clSetKernelArgSVMPointer(vectorAdd, 2, C);
    err |= clSetKernelArg(vectorAdd, 3, sizeof(unsigned int), &vectorSize);

    LARGE_INTEGER freq, start, stop;
    QueryPerformanceFrequency(&freq);

    // --- リスト5: GPUでの実行時間を計測 ---
    QueryPerformanceCounter(&start);
    // カーネルを命令キューに追加して実行
    err = clEnqueueNDRangeKernel(queue, vectorAdd, 1, NULL, &globalSize, &localSize, 0, NULL, NULL);
    // 命令キューの終了を待つ
    err = clFinish(queue);
    QueryPerformanceCounter(&stop);
    printf("GPU %f msec\n", 1000.0 * (double)(stop.QuadPart - start.QuadPart) / freq.QuadPart);

    // --- GPUの終了を待った後、CPUがメモリを触るための許可を再取得する ---
    clEnqueueSVMMap(queue, CL_TRUE, CL_MAP_READ | CL_MAP_WRITE, A, sizeof(double) * VECTOR_SIZE, 0, NULL, NULL);
    clEnqueueSVMMap(queue, CL_TRUE, CL_MAP_READ | CL_MAP_WRITE, B, sizeof(double) * VECTOR_SIZE, 0, NULL, NULL);
    clEnqueueSVMMap(queue, CL_TRUE, CL_MAP_READ | CL_MAP_WRITE, C, sizeof(double) * VECTOR_SIZE, 0, NULL, NULL);

    // --- リスト5: CPUで実行時間を計測 ---
    QueryPerformanceCounter(&start);
    for (int i = 0; i < VECTOR_SIZE; i++) {
        C[i] = A[i] + B[i];
    }
    QueryPerformanceCounter(&stop);
    printf("CPU %f msec\n", 1000.0 * (double)(stop.QuadPart - start.QuadPart) / freq.QuadPart);

    // --- リスト5: OpenMPで実行時間を計測 ---
    QueryPerformanceCounter(&start);
#pragma omp parallel for
    for (int i = 0; i < VECTOR_SIZE; i++) {
        C[i] = A[i] + B[i];
    }
    QueryPerformanceCounter(&stop);
    printf("OpenMP %f msec\n", 1000.0 * (double)(stop.QuadPart - start.QuadPart) / freq.QuadPart);

    // 解放
    clSVMFree(context, A);
    clSVMFree(context, B);
    clSVMFree(context, C);
    clReleaseKernel(vectorAdd);
    clReleaseProgram(program);
    clReleaseCommandQueue(queue);
    clReleaseContext(context);

    return 0;
}

実行結果 ↓

GPU 334.404900 msec
CPU 66.563400 msec
OpenMP 65.212900 msec

計算が簡単だったので、メモリの読み書きの速度で CPU が勝ったのかなと考察しました。
なので以下のように sin と cos の計算を加えて、重い処理に変えて実行してみました。

// --- リスト2: 配列同士の足し算をするカーネル ---
const char* kernelSource =
"__kernel void vectorAdd(__global double* a,          "
"                        __global double* b,          "
"                        __global double* c,          "
"                        const unsigned int n)        "
"{                                                    "
"    int id = get_global_id(0);                       "
"    if (id < n)                                      "
"        c[id] = a[id] + b[id];                       "
"        for (int k = 0; k < 100; k++) {              "
"            c[id] = sin(c[id]) + cos(c[id]);         "
"       }                                             "
"}                                                    ";
    // --- リスト5: CPUで実行時間を計測 ---
    QueryPerformanceCounter(&start);
    for (int i = 0; i < VECTOR_SIZE; i++) {
        C[i] = A[i] + B[i];
        for (int k = 0; k < 100; k++) {
            C[i] = sin(C[i]) + cos(C[i]);
        }
    }
    QueryPerformanceCounter(&stop);
    printf("CPU %f msec\n", 1000.0 * (double)(stop.QuadPart - start.QuadPart) / freq.QuadPart);

    // --- リスト5: OpenMPで実行時間を計測 ---
    QueryPerformanceCounter(&start);
#pragma omp parallel for
    for (int i = 0; i < VECTOR_SIZE; i++) {
        C[i] = A[i] + B[i];
        for (int k = 0; k < 100; k++) {
            C[i] = sin(C[i]) + cos(C[i]);
        }
    }
    QueryPerformanceCounter(&stop);
    printf("OpenMP %f msec\n", 1000.0 * (double)(stop.QuadPart - start.QuadPart) / freq.QuadPart);

実行結果 ↓

GPU 7035.128600 msec
CPU 82467.545500 msec
OpenMP 11607.859300 msec

実行速度は予想通り、iGPU > OpenMP(CPU並列)> CPU の順になりました。

カメラからの画像を iGPU でリアルタイム処理する

3つのフェーズでリアルタイム処理を実現します。

  • OpenCV(カメラ担当): カメラから今この瞬間の「1枚の写真」を撮る
  • OpenCL(加工担当): GPUのパワーを使って、その写真を猛スピードで加工する
  • OpenGL(テレビ担当): 加工が終わった写真を画面に映し出す

OpenCV をインストールする

.\vcpkg\vcpkg install opencv:x64-windows
.\vcpkg\vcpkg integrate install

OpenGL 用のライブラリ freeglut をインストールする

.\vcpkg\vcpkg install freeglut:x64-windows
.\vcpkg\vcpkg integrate install

コード

OpenCV 系で警告は出ましたが、以下のコードでエラーなく実行できました。

#define CL_TARGET_OPENCL_VERSION 200
#include <opencv2/opencv.hpp>
#include <CL/cl.h>
#include <GL/freeglut.h>
#include <iostream>
#include <vector>
#include <windows.h>

#pragma comment(lib, "OpenCL.lib")

// --- リスト6 & リスト8:ガウシアンおよびメディアン・フィルタのカーネル ---
const char* kernelSource =
"__constant sampler_t NEAREST = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;"
// リスト6: 3x3のガウシアン・フィルタ
"kernel void gaussian3x3(read_only image2d_t src, write_only image2d_t dst) {"
"    int x = get_global_id(0); int y = get_global_id(1);"
"    uint4 p[3][3];"
"    p[0][0] = read_imageui(src, NEAREST, (int2)(x-1, y-1)); p[0][1] = read_imageui(src, NEAREST, (int2)(x, y-1)); p[0][2] = read_imageui(src, NEAREST, (int2)(x+1, y-1));"
"    p[1][0] = read_imageui(src, NEAREST, (int2)(x-1, y));   p[1][1] = read_imageui(src, NEAREST, (int2)(x, y));   p[1][2] = read_imageui(src, NEAREST, (int2)(x+1, y));"
"    p[2][0] = read_imageui(src, NEAREST, (int2)(x-1, y+1)); p[2][1] = read_imageui(src, NEAREST, (int2)(x, y+1)); p[2][2] = read_imageui(src, NEAREST, (int2)(x+1, y+1));"
"    uint4 pixel;"
"    pixel.x = ((p[0][0].x + p[0][2].x + p[2][0].x + p[2][2].x) + (p[0][1].x + p[1][0].x + p[1][2].x + p[2][1].x)*2 + p[1][1].x*4) / 16;"
"    pixel.y = ((p[0][0].y + p[0][2].y + p[2][0].y + p[2][2].y) + (p[0][1].y + p[1][0].y + p[1][2].y + p[2][1].y)*2 + p[1][1].y*4) / 16;"
"    pixel.z = ((p[0][0].z + p[0][2].z + p[2][0].z + p[2][2].z) + (p[0][1].z + p[1][0].z + p[1][2].z + p[2][1].z)*2 + p[1][1].z*4) / 16;"
"    pixel.w = 255; write_imageui(dst, (int2)(x, y), pixel);"
"}"
// リスト8: メディアン・フィルタ
"kernel void median3x3(read_only image2d_t src, write_only image2d_t dst) {"
"    int x = get_global_id(0); int y = get_global_id(1);"
"    uint i, j, k; uint4 p[9], q[5], min;"
"    p[0] = read_imageui(src, NEAREST, (int2)(x-1, y-1)); p[1] = read_imageui(src, NEAREST, (int2)(x, y-1)); p[2] = read_imageui(src, NEAREST, (int2)(x+1, y-1));"
"    p[3] = read_imageui(src, NEAREST, (int2)(x-1, y));   p[4] = read_imageui(src, NEAREST, (int2)(x, y));   p[5] = read_imageui(src, NEAREST, (int2)(x+1, y));"
"    p[6] = read_imageui(src, NEAREST, (int2)(x-1, y+1)); p[7] = read_imageui(src, NEAREST, (int2)(x, y+1)); p[8] = read_imageui(src, NEAREST, (int2)(x+1, y+1));"
"    for(i=0; i<5; i++) { min=(uint4)(255,0,0,0); k=0; for(j=0; j<9; j++) { if(p[j].x < min.x) { min=p[j]; k=j; } } q[i]=min; p[k].x=255; }"
"    write_imageui(dst, (int2)(x, y), q[4]);"
"}"
// 5x5 メディアン(拡張)
"kernel void median5x5(read_only image2d_t src, write_only image2d_t dst) {"
"    int x = get_global_id(0); int y = get_global_id(1);"
"    uint i, j, k; uint4 p[25], q[13], min;"
"    k=0; for(int jj=-2; jj<=2; jj++) for(int ii=-2; ii<=2; ii++) p[k++] = read_imageui(src, NEAREST, (int2)(x+ii, y+jj));"
"    for(i=0; i<13; i++) { min=(uint4)(255,0,0,0); k=0; for(j=0; j<25; j++) { if(p[j].x < min.x) { min=p[j]; k=j; } } q[i]=min; p[k].x=255; }"
"    write_imageui(dst, (int2)(x, y), q[12]);"
"}";

// リスト9準拠の列挙型 (Windowsとの衝突回避のため接頭辞付与)
enum filter_type { FILTER_NONE, FILTER_GAUSSIAN3x3, FILTER_MEDIAN3x3, FILTER_MEDIAN5x5 };
filter_type current_filter = FILTER_NONE;
double msec = 0;

// グローバル
cv::VideoCapture camera(0);
cl_context context; cl_command_queue queue;
cl_kernel kGaus, kMed3, kMed5, activeK = NULL;
cl_mem input, output;
int width, height;

// --- リスト7:画像処理の演算フロー ---
void display() {
    cv::Mat image, rgba;
    camera.read(image);
    if (image.empty()) return;
    cv::cvtColor(image, rgba, cv::COLOR_BGR2BGRA); // 32ビットBGRA

    if (current_filter != FILTER_NONE && activeK != NULL) {
        LARGE_INTEGER freq, start, stop;
        QueryPerformanceFrequency(&freq);
        QueryPerformanceCounter(&start);

        size_t origin[3] = { 0, 0, 0 }, region[3] = { (size_t)width, (size_t)height, 1 };
        cl_event wait, finish;

        // clEnqueueWriteImage
        clEnqueueWriteImage(queue, input, CL_FALSE, origin, region, 0, 0, rgba.data, 0, NULL, &wait);
        // clEnqueueNDRangeKernel
        size_t global[2] = { (size_t)width, (size_t)height };
        clEnqueueNDRangeKernel(queue, activeK, 2, NULL, global, NULL, 1, &wait, &finish);
        // clEnqueueReadImage
        clEnqueueReadImage(queue, output, CL_TRUE, origin, region, 0, 0, rgba.data, 1, &finish, NULL);

        QueryPerformanceCounter(&stop);
        msec = 1000.0 * (double)(stop.QuadPart - start.QuadPart) / freq.QuadPart;
    }

    // 表示処理
    glClear(GL_COLOR_BUFFER_BIT);
    glPixelZoom(1.0, -1.0); glRasterPos2f(-1.0, 1.0);
    glDrawPixels(width, height, GL_BGRA_EXT, GL_UNSIGNED_BYTE, rgba.data);
    glutSwapBuffers();
    glutPostRedisplay();
}

// --- リスト9:キー入力処理 ---
void keyboard(unsigned char key, int x, int y) {
    switch (key) {
    case 'Q': case 'q': exit(0); break;
    case 'n': case 'N': current_filter = FILTER_NONE; puts("Filter:NONE"); break;
    case 'g': case 'G': current_filter = FILTER_GAUSSIAN3x3; activeK = kGaus; puts("Filter:Gaussian 3x3"); break;
    case '3': current_filter = FILTER_MEDIAN3x3; activeK = kMed3; puts("Filter:Median 3x3"); break;
    case '5': current_filter = FILTER_MEDIAN5x5; activeK = kMed5; puts("Filter:Median 5x5"); break;
    case 'f': printf("%f msec\n", msec); break;
    }
}

int main(int argc, char** argv) {
    if (!camera.isOpened()) return -1;
    width = (int)camera.get(cv::CAP_PROP_FRAME_WIDTH);
    height = (int)camera.get(cv::CAP_PROP_FRAME_HEIGHT);

    cl_platform_id p; clGetPlatformIDs(1, &p, NULL);
    cl_device_id d; clGetDeviceIDs(p, CL_DEVICE_TYPE_GPU, 1, &d, NULL);
    context = clCreateContext(NULL, 1, &d, NULL, NULL, NULL);
    queue = clCreateCommandQueueWithProperties(context, d, NULL, NULL);

    cl_image_format fmt = { CL_BGRA, CL_UNSIGNED_INT8 };
    cl_image_desc desc = { CL_MEM_OBJECT_IMAGE2D, (size_t)width, (size_t)height, 0, 0, 0, 0, 0, 0 };
    input = clCreateImage(context, CL_MEM_READ_ONLY, &fmt, &desc, NULL, NULL);
    output = clCreateImage(context, CL_MEM_WRITE_ONLY, &fmt, &desc, NULL, NULL);

    cl_program prog = clCreateProgramWithSource(context, 1, &kernelSource, NULL, NULL);
    clBuildProgram(prog, 1, &d, NULL, NULL, NULL);
    kGaus = clCreateKernel(prog, "gaussian3x3", NULL);
    kMed3 = clCreateKernel(prog, "median3x3", NULL);
    kMed5 = clCreateKernel(prog, "median5x5", NULL);

    clSetKernelArg(kGaus, 0, sizeof(cl_mem), &input); clSetKernelArg(kGaus, 1, sizeof(cl_mem), &output);
    clSetKernelArg(kMed3, 0, sizeof(cl_mem), &input); clSetKernelArg(kMed3, 1, sizeof(cl_mem), &output);
    clSetKernelArg(kMed5, 0, sizeof(cl_mem), &input); clSetKernelArg(kMed5, 1, sizeof(cl_mem), &output);

    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowSize(width, height);
    glutCreateWindow("OpenCL Realtime Filter (Interface)");

    glutDisplayFunc(display);
    glutKeyboardFunc(keyboard);
    glutMainLoop();
    return 0;
}
0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?