Pesquisa

só um divisor

Utilizando o Python para rodar no Torque ou PBS

Introdução ao Torque

Antes de ver a forma do script que utilizaremos para rodar no cluster devemos ter uma noção mínima do funcionamento do torque. O torque é um sistema de gerenciamento de filas o qual consiste de um computador mestre + vários nós. Quando entramos no cluster na realidade entramos no computador mestre: minerva, jarvis01, harpia01, minuano, etc. são os nomes dos computadores mestres. Estes computadores tem acesso à internet e acesso a una intranet onde estão os nós. O servidor tem um torque_serv que se encarrega de cuidar que os programas que estão rodando respeitem os limites impostos nas filas aos quais foram alocados e também tem um serviço que administra a colocação dos programas submetidos na fila adequada respeitando as regras (torque_scheduler ou maui).

Submetendo um programa à fila

Para submeter um programa à fila temos que criar um script em Bash que informara ao scheduler qual é a fila mais apropriada que satisfaça os recursos solicitados. Existem inúmeras opções de se construir o script mais as mais utilizadas (e adequadas para nos) são as seguintes

        #!/bin/bash

        #PBS -j oe
        #PBS -l nodes=1
        #PBS -l ncpus=1
        #PBS -l walltime=00:10:00
        #PBS -N teste1

        work_dir=$PBS_O_WORKDIR
        cd $work_dir
        echo val1 val2 val3 | ./executavel
        
onde informamos o tempo que o programa vai rodar (aproximadamente - chuta para mais). Um outro jeito igualmente muito utilizado é
        #!/bin/bash

        #PBS -j oe
        #PBS -l nodes=1
        #PBS -l ncpus=1
        #PBS -q um_mes
        #PBS -N teste1

        work_dir=$PBS_O_WORKDIR
        cd $work_dir
        echo val1 val2 val3 | ./executavel
        
onde dizemos qual é a fila (linha 6) no lugar do tempo. Vamos dissecar os scripts acima: Para rodar o script agora digitamos no terminal: qsub nomeScript:
[esalcedo@jarvis01: ]# qsub meuScript
78792.jarvis01
o número que devolveu antes do nome do servidor é o numero de processo.

Como saber as filas que o servidor possui

Para saber o nome (e o tempo máximo permitido por fila) devemos digitar no terminal o comando qstat -q, como se mostra na figura a seguir:

A informações que temos por coluna são

  1. Nome da fila.
  2. Quantidade de memoria máxima permitida por fila (que aqui não tem máximo).
  3. Máxima quantidade de tempo que é permitido rodar.
  4. Tempo real (medido por um relógio na parede) que o processo pode utilizar (não foi definido esse tempo).
  5. Nós associados à fila (a principio qualquer nó pode rodar processo de qualquer fila).
  6. Número de processos rodando na fila (cada fila tem um máximo permitido).
  7. Número de processos em espera.

Manipulando as filas

Uma vez submetido o programa à fila temos que cuidar dele, para isso o torque disponibiliza de alguns programas que ajudam nesse sentido. Para ver quais processos estão na fila temos dois comandos: qstat é o comando do torque que mostra toda os processos teus na fila:

A informações que temos por coluna são

  1. Número de processo.
  2. Nome do processo (dado pela opção #PBS -N ) no script.
  3. Nome de usuário dono do processo.
  4. Tempo total utilizado pelo processo (acredito que tempo de cpu).
  5. Estado do processo ($C$ para completo, $R$ para rodando, $Q$ para na fila - queue, $E$ para erro).
  6. Fila na qual está rodando.
Outra forma de se obter informação sobre os processos na fila é utilizando o comando showq, mas nem todas os sistemas de filas possuem este comando habilitado (tem que estar instalado o maui, caso contrario só utilize o comando acima citado)

Observe que as informações obtidas são as mesma do que com o qstat, mas numa outra formatação.

Removendo um programa da fila

Para remover um programa da fila o comando utilizado é o qdel:

No exemplo acima foi deletado o processo 78807, nota que antes de ser removido ele passa para o estado E, depois passa para C. Isso acontece porque o servidor tem que se comunicar com o nó onde o processo está rodando e esperar a confirmação de morte do processo.

Submetendo muitos processos simultaneamente

O cluster me permite submeter cento ou miles de processos simultaneamente (na verdade na jarvis01 o máximo soft é 12 => 3 computadores, e o hard é 8 => 2 computadores; contudo não há limite para processos na espera) e isto é muito útil nas pesquisas em mecânica estatística, contudo resulta inadequado criar centos de script e submeter, as chances de cometer erro aumenta em proporção exponencial com o número de processos submetidos. Para diminuir estas chances temos que criar um script em alguma linguagem de script que crie os centos ou miles de scripts de que preciso e submeta eles às filas. Note que eu falei às filas, isto porque cada fila tem um máximo de processos rodando por usuário permitido (na jarvis01: small -> 8, large -> 4, infinity -> 2), mas se eu puder ir além do máximo devo utilizar mais de uma fila simultaneamente (há filas que podem limitar o número de usuários, por exemplo a infinity limita a 3 usuários => 1.5 nós).

A linguagem que escolhi para fazer o criador de script é o Python, poderia ser o Perl também ou mesmo o Bash (mas trabalhoso, mas é possível). A ideia é criar varias vetores dos diversos parâmetros de entrada do programa. Depois criar vários loops com os diferentes dados e a partir destes criar os scripts que serão submetidos à fila, com esses mesmos dados será construído o nome do script (nome do arquivo) os quais serão armazenados num outro vetor, no final se utiliza a função popen do Python para rodar cada um dos arquivos com os script.

Para montar o script vamos supor que temos um programa que pede como parâmetros de entrada na seguente ordem: semente do gerador de números aleatórios, pressão, velocidade, concentração, coeficiente de atrito (esses dados se referem a um sistema granular sujeito a pressas e que se desloca com velocidade constante):

      #!/usr/bin/env python
      # -*- coding: utf-8 -*-

      import os, stat, subprocess, glob, re, datetime, time
      #from subprocess import Popen, call, PIPE
      import numpy as np


      #***********************************************
      
      mu       = '0.60'
      Pmin     = 0.00
      Pmax     = 0.20
      Dp       = 0.010
      Vmin     = 0.005
      Vmax     = 0.07
      Dv       = 0.005
      PorMax   = 30.0
      PorMin   = 10.0
      Dpor     = 5.0
      NumFilas = 1
      MaxRuns  = 16

      Fila    = [ 'small', 'large' ]

      Sem = ['19438']

      Pres = ['0.20', '0.30', '0.40']

      Vel  = []
      v    = Vmin
      while (v < Vmax):
        v = v + Dv
        a = '%.3f' % v
        Vel.append(a)
        
      Por  = []
      po   = PorMin
      while (po < PorMax):
        po = po + Dpor
        if (po < 10.0):
          a = '0%.1f' % po
        else:
          a = '%.1f' % po
        print a
        Por.append(a)

      job     = []
      chaves  = ['Mu', 'Pres', 'Vel', 'Porc', 'Name', 'Job', 'DirJob', 'Semente', 'Fila']
      cont    = 0
      for s in Sem:
        for p in Pres:
          for po in Por:
            for v in Vel:

              JobName  = 'friccion_sem' + s + '_mu' + mu + '_por' + po + '_pres' + p + '_vel'  + v
              Job      = JobName + '.psh'
              DirJob   = 'dirjob_' + JobName

              checkfile = os.path.join(DirJob,'terminoDeRodar.status')

              if (not os.path.isfile(checkfile)):
                fila = cont % NumFilas
                fila = Fila[fila]
                cont = cont + 1
                val  = [mu, p, v, po, JobName, Job, DirJob, s, fila]
                job.append( dict( zip(chaves, val) ) )
     
        if (len(job) > MaxRuns):
          NumJob = MaxRuns
        else:
          NumJob = len(job)
          
        for i in range(NumJob):
          print i
          ScriptToRun = open(job[i]['Job'], 'w')

          print >> ScriptToRun,  "#!/bin/bash"
          print >> ScriptToRun,  "\n"
          print >> ScriptToRun,  "#PBS -j oe"
          print >> ScriptToRun,  "#PBS -l ncpus=1"
          print >> ScriptToRun,  "#PBS -q %s" % job[i]['Fila']
          print >> ScriptToRun,  "#PBS -N %s" % job[i]['Name']
          print >> ScriptToRun,  "\n"
          print >> ScriptToRun,  "work_dir=$PBS_O_WORKDIR/%s" % job[i]['DirJob']
          print >> ScriptToRun,  "mkdir $work_dir"
          print >> ScriptToRun,  "\n"
          print >> ScriptToRun,  "ln -sf $PBS_O_WORKDIR/friccion_run $work_dir"
          print >> ScriptToRun,  "cp $PBS_O_WORKDIR/base $work_dir"
          print >> ScriptToRun,  "\n"
          print >> ScriptToRun,  "cd $work_dir"
          print >> ScriptToRun,  "\n"
          print >> ScriptToRun,  "export LC_ALL=C"
          print >> ScriptToRun,  "\n"
          print >> ScriptToRun,  "echo %s %s %s %s %s | ./friccion_run" % (job[i]['Semente'], job[i]['Pres'], job[i]['Vel'], job[i]['Porc'], job[i]['Mu'])

          ScriptToRun.close()
          os.chmod( job[i]['Job'], stat.S_IRWXU)


          ScriptToRun = 'qsub ./' + job[i]['Job']
          Submete = subprocess.Popen( [ ScriptToRun ], stdout=subprocess.PIPE, shell=True )
          Submete.wait()
          SaidaSubmete = Submete.communicate()[0]     
      

Observe que na linha 60 é checado que foi criado o arquivo terminoDeRodar.status. Isso porque em geral meu programas geram dois arquivos, logo que incia a rodar ele cria inicioARodar.status onde é guardada a data e hora em que o programa foi rodado:

        cat inicioARodar.status
        14/08/2013 as 08:05:00:707
        
e logo antes do fim do programa ele grava o arquivo terminoDeRodar.status, onde é armazenada a data e hora do fim do programa:
        cat terminoDeRodar.status
        14/08/2013 as 08:07:50:856
        
Para criar esse arquivos foi utilizada a função do fortran data_and_time, exemplo:
        Program imprimeTempo
          Implicit None
          
          Integer, Dimension(8) :: time_array, time_array1
          
          call date_and_time(values=time_array)
          
          Open(Unit = 30, File='inicioARodar.status', Status='Replace')
          Write(Unit = 30, Fmt='(I2.2,A1,I2.2,A1,I4,X,A2,X,I2.2,A1,I2.2,A1,I2.2,A1,I3.3)') time_array(3), '/',  time_array(2), '/',&
                time_array(1), 'as', time_array(5),':', time_array(6),':', time_array(7), ':',time_array(8)
          Close(Unit=30)
          
        End Program imprimeTempo
        

Para matar todos os processos da fila

      #!/usr/bin/env python
      # -*- coding: iso-8859-15 -*-

      import os, stat, subprocess, glob, re
      #from subprocess import Popen, call, PIPE
      import numpy as np

      #***********************************************

      processos = 'qstat'
      CapturaProcessos = subprocess.Popen( [ processos ], stdout=subprocess.PIPE, shell=True )
      #CapturaProcessos.wait()
      SaidaCaptura = CapturaProcessos.communicate()[0]
      Parte = re.split(r"\n", SaidaCaptura)
      for i in range( len(Parte) ):
          if (i > 1):
              Parte2 = Parte[i].split()
              ProcesoParaApagar =  Parte2[0]
              print ProcesoParaApagar
              Comando = 'qdel ' + ProcesoParaApagar
              PythonExecuta = subprocess.Popen( [ Comando ], stdout=subprocess.PIPE, shell=True )
              SaidaMata = PythonExecuta.communicate()[0]
              print SaidaMata
      

Para matar uma sequencia de processos da fila

        for i in `seq INICIO FIM`; do qdel $i; done