Использование узлов с Intel Xeon Phi

  • 'Intel MIC' (Many Integrated Core) - архитектура многоядерной системы, основанная на классической архитектуре x86.
  • 'Xeon Phi' - бренд для линейки продуктов Intel на архитектуре MIC.
  • 'MPSS' (Manycore Platform Software Stack) - набор драйверов и утилит от Intel, необходимый для использования сопроцессоров Xeon Phi. Версию установленного пакета MPSS видно в директории '/opt/mpss' на узлах с Xeon Phi. На момент написания данной документации используется версия 3.7.
  • Написанное ниже относится ко второму поколению архитектуры Intel MIC с кодовым названием 'Knights Corner' (KNC).
  • Сопроцессор Xeon Phi представляет из себя устройство, подключаемое к компьютеру (называемому 'хост') через шину PCI Express и работающее под управлением собственной операционной системы, основанной на ядре Linux. Сопроцессор не имеет жёсткого диска и файловая система занимает место в его оперативной памяти, поэтому на сопроцессоре установлен минимум дополнительного программного обеспечения. Пользовательское окружение реализовано с помощью пакета BusyBox. С хоста сопроцессор доступен по протоколу SSH через виртуальную локальную сеть. Операционная система сопроцессоров настроена так, что имеется доступ к домашним директориям и рабочим областям пользователей.
  • Ожидается, что следующие поколения Xeon Phi смогут использоваться как полноценные CPU, устанавливаемые непосредственно в сокет на материнской плате.
  • Цитата с сайта Intel: 'Чтобы полностью раскрыть преимущества сопроцессоров Intel Xeon Phi, приложение должно масштабироваться до уровня более 100 программных потоков, а также либо активно использовать векторные вычисления, либо эффективно использовать полосу пропускания локальной памяти, более широкую, чем у процессора Intel Xeon.'
  • На данный момент у нас имеется 5 серверов HP XL250a Gen9, каждый из которых содержит:
    • Два 12-ядерных процессора Intel Xeon E5-2680v3 с тактовой частотой 2500 МГц
    • 192 ГБ ОЗУ
    • Два сопроцессора Intel Xeon Phi 7120P, у каждого из которых:
      • 16 ГБ собственного ОЗУ
      • 61 ядро, работающее в 244 потока на частотах 1,2 - 1,3 ГГц
      • 1,2 Тфлопс пиковой производительности для вычислений с двойной точностью
  • Узлы с Xeon Phi выделены в отдельную очередь с именем 'xl250g9q'. Чтобы задача запускалась на этих узлах, в скрипт для qsub необходимо добавить такую строку:
    #PBS -q xl250g9q
  • Каждый сопроцессор должен использоваться только одной задачей одновременно. При этом наличие двух сопроцессоров позволяет запускать на узле одновременно две задачи. Для этого каждый узел делится c точки зрения PBS на два виртуальных узла, называемых 'vnode' (аналогично узлам с GPU). Каждый из виртуальных узлов содержит один сопроцессор, 12 ядер центрального процессора и 96 ГБ ОЗУ.
  • Для запроса необходимого количества сопроцессоров используется ресурс 'nmics'. Например, так:
    #PBS -l select=1:ncpus=4:mem=12gb:nmics=1
  • Все запрашиваемые ресурсы (количество ядер, mpi-/openmp- процессов, ОЗУ, …) относятся к хосту. Сопроцессор выделяется в монопольное использование и вы можете запускать на нём любое разумное количество процессов.
  • Обращаем внимание, что название 'виртуальный узел' подразумевает только логическое выделение части ресурсов физического сервера с точки зрения планировщика PBS, но не использование виртуализации. Например, при выполнении указанного выше запроса на сервере останутся неиспользумыми: второй сопроцессор, 20 ядер CPU и 180ГБ ОЗУ. И все эти ресурсы доступны другим задачам.
  • После запуска задачи она должна узнать, какой виртуальный узел (или узлы) ей выделен и по его названию понять порядковый номер сопроцессора, который она должна использовать. Например, виртуальные узлы сервера cn331 называются cn331[0] и cn331[1]. Если задаче выделен виртуальный узел cn331[1], значит, она должна использовать именно сопроцессор №1 на узле cn331.
  • Определить предоставленный виртуальный узел можно с помощью значения параметра 'exec_vnode' в выводе команды 'qstat -f $PBS_JOBID'. Данную операцию можно произвести как из скрипта (примеры приведены ниже), так и из самого запускаемого приложения, если имеется возможность модифицировать его код. Следует учитывать, что при запросе нескольких сопроцессоров вывод значения 'exec_vnode' может занимать более одной строки.
  • :!: Автоматического ограничения доступа к остальным сопроцессорам узла средствами PBS или драйверов Xeon Phi не происходит, пользователь должен сам настраивать свою задачу для использования именно тех сопроцессоров, которые ей выделены планировщиком.


Пример 1:

  • В случае, если запрашивается только один сопроцессор, можно быть уверенным, что значение 'exec_vnode' поместится в одну строку. Кроме того, нет необходимости определять, какой физический сервер нам выделен, т.к. именно на нём задача и запустится.
  • Код скрипта, определяющего номер предоставленного сопроцессора и его имя:
    #!/bin/sh
    
    #PBS -q xl250g9q
    #PBS -l walltime=0:01:00
    #PBS -l select=1:nmics=1:ncpus=1:mem=1gb
    
    cd $PBS_O_WORKDIR
    vnodes=$(qstat -f $PBS_JOBID|grep exec_vnode|sed -e 's/ *//')
    echo "$vnodes"
    if [ $(echo $vnodes|grep -c '+') != 0 ] ; then
        echo "Error: several vnodes are provided."
        exit 100
    fi
    mic=$(echo $vnodes|sed 's/.*\[//'|sed 's/\].*//')
    echo "mic=$mic"
    micinfo -deviceInfo $mic|grep 'Device Name'
  • После передачи этого скрипта команде qsub получим в файле стандартного вывода примерно следующее:
    exec_vnode = (cn332[0]:ncpus=1:mem=102400kb:nmics=1)
    mic=0
    Device No: 0, Device Name: mic0

    В данном случае задаче был предоставлен сопроцессор с номером '0' и именем 'mic0'


Пример 2:

  • Если задаче требуется несколько сопроцессоров, анализ предоставленных виртуальных узлов будет немного сложнее:
    #!/bin/sh
    
    #PBS -q xl250g9q
    #PBS -l walltime=0:01:00
    #PBS -l select=3:nmics=2:ncpus=4:mem=4gb
    
    cd $PBS_O_WORKDIR
    vnodes=$(qstat -f $PBS_JOBID|tr -d '\n'' ''\t'|sed 's/Hold_Types.*//'|sed 's/.*exec_vnode=//'|tr -d \(\)|tr + '\n'|sed 's/:.*//'|sort)
    echo "My vnodes:"
    for vnode in $vnodes ; do
        node=$(echo $vnode|sed 's/\[.*//')
        mic=$(echo $vnode|sed 's/.*\[//'|sed 's/\]//')
        echo "$vnode = Node $node, mic $mic"
    done
  • В результате работы получим в файле стандартного вывода примерно следующее:
    My vnodes:
    cn332[0] = Node cn332, mic 0
    cn332[1] = Node cn332, mic 1
    cn333[0] = Node cn333, mic 0
    cn333[1] = Node cn333, mic 1
    cn334[0] = Node cn334, mic 0
    cn334[1] = Node cn334, mic 1
  • Для компиляции программ будут использоваться компиляторы Intel. Для настройки необходимых переменных окружения перед использованием нужно выполнять:
    source /opt/intel/composerxe/bin/compilervars.sh intel64

    Для изучения возможных нюансов будет полезно просмотреть вывод комадны 'man' на используемый компилятор (icc / icpc / ifort) и изучить параметры компиляции и переменные окружения, влияющие на генерацию кода и работу приложений для архитектуры Intel MIC.

  • Существуют три модели использования сопроцессора:
    • Offload. Программа работает на основном компьютере, но некоторые её части выполняются на сопроцессоре.
    • Native. Программа работает только на сопроцессоре и не взаимодействует с основным компьютером.
    • Symmetric. Нагрузка разделена между основным компьютером и сопроцессором. Например, это может быть программа, использующая стандарт MPI и работающая как на хосте, так и на сопроцессоре.

Offload

  • Для выделения участка кода, который должен выполняться на сопроцессоре, используется директива #pragma offload target(mic:номер), где 'номер' - номер используемого сопроцессора (в нашем случае это '0' или '1', в зависимости от виртуального узла, выделенного PBS ). Параметр 'номер' не является обязательным с точки зрения синтаксиса, но при работе на нашем кластера он должен указываться и сответствовать номеру сопроцессора, предоставленному планировщиком задач. Также эта директива может содержать дополнительные параметры, например, указывающие, значения каких переменных должны передаваться между ОЗУ узла и собственной памятью сопроцессора:
    • in - перед началом выполнения кода на сопроцессоре значения указанных переменных будут скопированы из памяти компьютера в память сопроцессора.
    • out - после окончания выполнения кода на сопроцессоре значения указанных переменных будут скопированы из памяти сопроцессора в память компьютера.
    • inout - перед началом выполнения кода на сопроцессоре значания указанных переменных будут скопированы из памяти компьютера в память сопроцессора, а после завершения - скопированы обратно.
  • При использовании компиляторов Intel выделенный планировщиком сопроцессор можно указывать не в директиве #pragma offload, а при помощи переменной окружения 'OFFLOAD_DEVICES'. Подробности смотрите в выводе команды 'man' на используемый компилятор.
  • На сопроцессоре код будет выполняться не из под вашей учетной записи, а с правами специального пользователя 'micuser'. Поэтому вы не сможете, например, модифицировать свои файлы в домашней директории.
  • Для распараллеливания можно использовать OpenMP внутри блока директивы #pragma offload. Также можно делать offload из MPI-процессов, работающих на хосте.
  • Компиляторы GCC поддерживают offloading только для сопроцессоров следующего поколения архитектуры MIC - 'Knights Landing' (KNL).


Пример:

  • Создать файл 'show_cores.c' такого содержания:
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main(int argc, char** argv)
    {
        int  cores;
        char host[32];
        char * endptr;
        long mic_number;
    
        if (argc < 2) {
            printf("ERROR: MIC number absent!\n");
            exit(1);
        }
        mic_number = strtol (argv[1], &endptr, 10);
        if (endptr == argv[1] || (mic_number != 0 && mic_number != 1) ) {
            printf("ERROR: incorrect MIC number!\n");
            exit(2);
        }
        printf("mic = %i\n",mic_number);
    
        gethostname(host,32);
        cores = sysconf( _SC_NPROCESSORS_ONLN );
        printf("hostname = %s, logical cores = %d\n",host,cores);
    
        #pragma offload target(mic:mic_number) out(host,cores)
        {
            gethostname(host,32);
            cores = sysconf( _SC_NPROCESSORS_ONLN );
        }
        printf("micname = %s, logical cores = %d\n",host,cores);
    
        return 0;
    }
  • Для преобразования полученного в качестве параметра номера сопроцессора в число вместо часто используемой функции 'atoi' используется 'strtol'. Существенный недостаток 'atoi' состоит в том, что в случае ошибки (например, в качестве параметра передано не число) поведение этой функции и возвращаемое значение неопределены. В лучшем случае будет возвращено значение '0', что может привести к некорректному выбору сопроцессора с номером '0'.
  • Для компиляции и выполнения будет использоваться такой скрипт для qsub:
    #!/bin/sh
    
    #PBS -q xl250g9q
    #PBS -l walltime=0:01:00
    #PBS -l select=1:ncpus=1:mem=100mb:nmics=1
    
    cd $PBS_O_WORKDIR
    
    vnodes=$(qstat -f $PBS_JOBID|grep exec_vnode|sed -e 's/ *//')
    if [ $(echo $vnodes|grep -c '+') != 0 ] ; then
        echo "Error: several vnodes are provided."
        exit 100
    fi
    mic=$(echo $vnodes|sed 's/.*\[//'|sed 's/\].*//')
    
    source /opt/intel/composerxe/bin/compilervars.sh intel64
    icc -o show_cores show_cores.c
    ./show_cores $mic
  • После завершения задачи в файле стандартного потока вывода будут находиться имена сервера и сопроцессора, а также количество их логических ядер, примерно так:
    mic = 0
    hostname = cn332, logical cores = 24
    micname = cn332-mic0, logical cores = 244

Native

  • Для генерации исполняемого кода, способного работать на архитектуре MIC, компиляторам Intel необходимо указывать параметр '-mmic'
  • Вместо компиляторов Intel можно использовать GCC, поставляемый Intel в составе пакета MPSS. Более подробное описание находится в MPSS Users Guide.
  • Полученный при компиляции файл сразу становится виден с сопроцессора, его не нужно туда копировать.
  • Запуск программы в native режиме производится из скрипта для qsub командой 'ssh' с указанием в качестве параметров имени используемого сопроцессора (mic0 или mic1, в зависимости от выделенного планировщиком виртуального узла) и набора команд, которые должны быть выполнены на сопроцессоре.
  • Программы в режиме native могут использовать как OpenMP, так и MPI.


Пример:

  • Создать файл 'show_cores_natively.c' такого содержания:
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char** argv)
    {
        int  cores;
        char host[32];
    
        gethostname(host,32);
        cores = sysconf( _SC_NPROCESSORS_ONLN );
        printf("micname=%s, logical cores=%d\n",host,cores);
    
        return 0;
    }
  • Как видно, это вполне обычный код, без какой-либо специфики архитектуры Intel MIC.
  • Для компиляции и выполнения использовать такой скрипт для qsub:
    #!/bin/sh
    
    #PBS -q xl250g9q
    #PBS -l walltime=0:01:00
    #PBS -l select=1:ncpus=1:mem=100mb:nmics=1
    
    cd $PBS_O_WORKDIR
    
    vnodes=$(qstat -f $PBS_JOBID|grep exec_vnode|sed -e 's/ *//')
    if [ $(echo $vnodes|grep -c '+') != 0 ] ; then
        echo "Error: several vnodes are provided."
        exit 100
    fi
    mic="mic$(echo $vnodes|sed 's/.*\[//'|sed 's/\].*//')"
    
    source /opt/intel/composerxe/bin/compilervars.sh intel64
    icc -mmic -o show_cores_natively show_cores_natively.c
    ssh -q $mic "cd $PBS_O_WORKDIR ; ./show_cores_natively"
  • После завершения задачи в файле стандартного потока вывода будет находиться примерно такое:
    micname=cn333-mic0, logical cores=244

Symmetric

  • В данном разделе рассматривается реализация симметричого режима при помощи стандарта MPI.
  • Будет использоваться последняя имеющаяся у нас версия Intel MPI, поэтому на интерфейсном сервере необходимо выполнить:
    mpi-selector --set intel_mpi-5.0.1.035
  • Создать файл 'hello_mpi_world.c' такого вида:
    #include <mpi.h>
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char** argv)
    {
        int  size, rank;
        char host[32];
    
        MPI_Init(&argc,&argv);
        MPI_Comm_size(MPI_COMM_WORLD,&size);
        MPI_Comm_rank(MPI_COMM_WORLD,&rank);
    
        gethostname(host,32);
        printf("Hello, MPI world! I'm number %d from %d and I run on host %s\n",rank,size,host);
    
        MPI_Finalize();
        return 0;
    }
  • Этот файл необходимо скомпилировать два раза и получить исполняемые версии для хоста и для сопроцессора. Это можно сделать с помощью такого скрипта для qsub:
    #!/bin/sh
    #PBS -l select=1:ncpus=1:mem=1g
    #PBS -l walltime=00:01:00
    #PBS -q xl250g9q
    
    cd $PBS_O_WORKDIR
    source /opt/intel/composerxe/bin/compilervars.sh intel64
    mpiicc hello_mpi_world.c -o hello_mpi_world
    mpiicc -mmic hello_mpi_world.c -o hello_mpi_world.mic
  • Обратите внимание, что компиляция производится командой mpiicc (использующей icc), а не mpicc (по умолчанию использующей gcc). Подробнее см. на странице с описанием Intel MPI. В результате работы этой задачи должны получиться два исполняемых файла: hello_mpi_world (для запуска на хосте) и hello_mpi_world.mic (для запуска на сопроцессоре).
  • Команде mpirun потребуется файл с описанием, сколько MPI-процессов на каких серверах и сопроцессорах нужно запускать. Это файл будет сообщаться mpirun через параметр '-machinefile'. Формат файла простой: каждому процессу соответствует одна строка, содержащая имя сервера, на котором этот процесс должен запускаться. Либо в строке после имени сервера через двоеточие может находиться число, указывающее, сколько потоков надо запустить на этом сервере. Например, так:
    cn331
    cn331
    mic0:3

    Первая строка соответствует процессу с рангом '0'. При использовании MPI на обычных серверах, без сопроцессоров, этот файл создаётся PBS на основе запроса 'select=' и путь до него находится в переменной окружения PBS_NODEFILE. Но при использовании MPI на сопроцессорах в запросе не указывается, сколько процессов будет запускаться на сопроцессоре. Поэтому файл надо будет создать самостоятельно, взяв за основу предоставленный PBS и добавив к нему нужное количество строк, соответствующих сопроцессору.

  • В скрипте обязательно должна устанавливаться переменная окружения, разрешающая MPI-взаимодействие между хостом и сопроцессором:
    export I_MPI_MIC=enable
  • Также необходимо указать, что имя исполняемого файла для запуска на сопроцессоре отличается от имёни файла для хоста расширением '.mic':
    export I_MPI_MIC_POSTFIX=.mic


Пример:

  • Все MPI-потоки находятся в пределах одного сервера, используется только один сопроцессор с этого же сервера.
  • Для запуска задачи используется такой скрипт для qsub:
    #!/bin/sh
    #PBS -l select=1:ncpus=2:mpiprocs=2:mem=2g:nmics=1
    #PBS -l walltime=00:01:00
    #PBS -q xl250g9q
    
    cd $PBS_O_WORKDIR
    source /opt/intel/composerxe/bin/compilervars.sh intel64
    
    # Create machinefile:
    machinefile=$PBS_O_WORKDIR/machinefile.$(echo $PBS_JOBID|awk -F. '{print $1}')
    cat $PBS_NODEFILE > $machinefile
    vnodes=$(qstat -f $PBS_JOBID|grep exec_vnode|sed -e 's/ *//')
    if [ $(echo $vnodes|grep -c '+') != 0 ] ; then
        echo "Error: several vnodes are provided."
        exit 100
    fi
    mic="mic$(echo $vnodes|sed 's/.*\[//'|sed 's/\].*//')"
    # Add 3 MPI processes for Xeon Phi:
    echo "${mic}:3" >> $machinefile
    
    export I_MPI_MIC=enable
    export I_MPI_MIC_POSTFIX=.mic
    
    mpirun -machinefile $machinefile $PBS_O_WORKDIR/hello_mpi_world
    rm $machinefile
  • В результате работы этой задачи получим в файле потока стандартного вывода примерно такое:
    Hello, MPI world! I'm number 1 from 5 and I run on host cn332
    Hello, MPI world! I'm number 0 from 5 and I run on host cn332
    Hello, MPI world! I'm number 2 from 5 and I run on host cn332-mic0
    Hello, MPI world! I'm number 3 from 5 and I run on host cn332-mic0
    Hello, MPI world! I'm number 4 from 5 and I run on host cn332-mic0