EE 524 / CS 561 Spring, 2000 PROJECT 6 A Parallel and Synchronized Music Interpreter for the Beowulf Cluster Hye Kim, Scott Mantei & Llewellyn Yap Prepared for: Professor Mark L. Manwaring EECS, Washington State University Pullman, WA 99164 March 28, 2000 A Parallel and Synchronized Music Interpreter for the Beowulf Cluster Introduction The task at hand is to build a music generator that will run concurrently on the four-Beowulf cluster. Synchronization, hence, becomes an issue. This document reports on the algorithm and implementation of the music-generating program. Before any programming can begin, certain requirements are necessary. A sound generating function for the Linux OS must be available. One such module can be found in [1] (complements to Rama Shenai). Next, LAM/MPI must be accessible on the Beowulf. This condition has been satisfied since the last project. What remains is to piece together a concurrent program that will play synchronous melodies. Such a design is presented below. Parallel / Synchronizing Algorithm of the Music Interpreter Several assumptions were made before the design of the program. We attempted to synchronize 4-part harmony music, having each voice play at each Beowulf node (i.e. SATB on nodes n0, n1, n2, n3 respectively or in any order). The file format and music grammar will be discussed in a latter section. We decided that the best way to synchronize the processes is to use TIME as the measure. Since music is written in phrases, we thought that synchronizing at every bar should work. Shown below is the pseudo code of the program. Following that is a brief description of what it does. /* Pseudo Code for music_v4.c */ Define sound(freq, length) function (adapted from [1]) Begin main() { Initialize MPI; Determine which node you are (MPI_Comm_rank()); Open “/dev/console” file; {write to this file to play sound) If I’m the World Node { Open song file; Read song file & Distribute (write) parts to Arrays - n0, n1, n2, n3; Send appropriate array size and array to nodes; i.e. array n1 -> send to node1; array n2 -> send to node2; ... etc Receive Acknowledgement from each node; /* initial synchronization */ PLAY MUSIC! { As music being played, keep track of time increments; (i.e. accumulated sum of the delays) If (accumulated time == MILEPOST) Receive synchronizing flag from each node; Once all nodes reported in, Issue flag to continue playing; Reinit accumulated time = 0; Repeat & Play till music ends; } } If I’m NOT the World Node { Receive array size and array from World Node; Send Acknowledgement indicating reception of data and ready to play; PLAY MUSIC! { As music being played, keep track of time increments; If (accumulated time == MILEPOST) Send synchronizing flag to World Node; Wait to Receive Continue signal; Upon reception, continue with piece; Reinit accumulated time = 0; Repeat & Play until music ends; } } Exit_MPI(); } /* end main() */ The main gist of the program is described here. It begins by tagging each node with a rank. The World Node identifies itself and begins opening the song data file. It distributes its contents to 4 predefined structured arrays. After accomplishing this, it sends the appropriate array size and array to the rest of the Beowulf nodes. It waits for acknowledgements from the nodes. After this, it begins playing its part. As it plays, it keeps record of the time passing by. When time reaches a MILEPOST (a time to synchronize), it pends for synchronizing flags from each node. When all nodes are accounted for, it flags everyone to continue where they left off. This synchronization continues at every MILEPOST till the end of the music. For the other nodes, their jobs are simpler. They wait for song data from the World Node, and sends off the acknowledgement. Music plays. At a MILEPOST, they send a synchronizing flag to the World Node and wait for the continue flag, which will only arrive after the World Node receives flags from all nodes. Nodes continue playing and synchronizing until they reach the end of the music. The complete source code may be found in Appendix A. Appendix B holds the compilation and execution of the program. Music Grammar Implemented The music passage we are using for this project is “Oh Beautiful for Spacious Skies.” In choosing a piece, we searched for music that had four distinct parts, namely soprano, alto, tenor and bass. We looked at hymnals to find such a piece because hymnals are full of four-part music. Once the piece was decided upon, we translated the notes into a text file. The first column of the text file specified the node number. Every note that node 1 was to play was given the number 1, for example. The second column was the frequency value given in Hertz. And the third column was the duration of the note given in milliseconds. A sample of the data file is as follows: 1 392 250 1 392 375 1 330 125 : : 2 330 250 2 330 375 2 262 125 : Page 2 : 3 262 250 3 262 375 3 196 125 : : The result of this musical passage is all four nodes playing synchronously in four-part harmony. The program searches for a node number and copies the frequency and duration to a pre-defined data structure. So far, no actions have been taken to check for errors in the data file, except a requirement that the total time length of each node be exact. Tests & Results After some sticky situations with the sticky bit (as discussed in Appendix B), we were finally able to enjoy and be impressed by the 4-part harmony. The piece is finally parallelized and synchronized. At the start of program, there was some network activity as the World Node was sending data to the child nodes. For this “Oh Beautiful for Spacious Skies” piece, the MILEPOST was set at 1000ms (i.e. synchronization occurs every 1s). Network traffic during the play of the song, as observed at the hub, was very minimal (< 5%). The piece sounded smooth and fluent. Latency was not an issue. In order to determine if our synchronizing techniques really worked, we forced some of the nodes to play faster (or slower) than the rest (by multiplying the duration parameter by an appropriate factor). Without any synchronization (i.e. setting MILEPOST = large number), the piece sounded terrible. Applying synchronization, the song was synchronized at each MILEPOST and after that, it goes wild, but to again be synchronized at the next MILEPOST. Synchronization is hence achieved. Though these tests were not quantitative, the program does do a good stab at parallelizing and synchronizing our 4-part harmony. In fact, the human listener won’t be able detect the millisecond time-differences. So we feel justified for not doing a more formal analysis, and let our ears be the judge. Conclusions We were successful in implementing the music-generating program which plays up to 4 different voices synchronously. Music may be broken down into different parts by cleverly writing the music data file. The frequency of synchronization may be adjusted with the MILEPOST value. The speed of the song is set by the TEMPO value. Lengthy songs may be played by increasing the array size allocator, NOTE_BUF. Henceforth, users are encouraged to write their own music files and have the Beowulf entertain them for hours. References [1] console_beep-0.1.tar.gz (beep.c function). http://metalab.unc.edu/pub/Linux/apps/sound/misc/. Page 3 APPENDIX A SOURCE CODE (music_v4.c) /**********************************************************************/ /* File: x86wulf:/home/lyap1/LAM/PROJ6/music_v4.c /* /* Programmers: Llewellyn Yap, Scott Mantei, Hye Kim /* /* Inputs: Music file "obeautf.dat" (embedded in code) /* /* Output: Sound via internal speaker /* /* Requirements: This program requires a Beowulf cluster of 4 /* machines and a music file. The music file /* should be written in the format: /* /* ... node# freq duration ... /* /* (where node# = {1, 2, 3 or 4} /* /* Description: This program parallelizes / synchronizes 4/* part harmony on the Beowulf using MPI routines. /* The TEMPO determines the speed of the music. /* A TEMPO value < 1 increases the speed etc. /* MILESTONE determines how often the song should /* be synchronized (in ms). These parameters may /* change with different pieces. /* /* Date: 3-28-2000 /***********************************************************************/ #include <mpi.h> #include <stdio.h> #define TEMPO 1.0 #define MILEPOST 1000 #define NOTE_BUF 100 /* MULTIPLIER, speed of music */ /* synchronize at every MILEPOST ms */ /* limits notes to be stored */ typedef struct{ int freq; int length; }note; /* console_beep V0.1 "borrowed" from Josef Pavlik <jetset@ibm.net> */ FILE *dest; void sound(int freq, int length) { fprintf(dest,"\33[10;%d]\33[11;%d]\a\33[10]\33[11]", freq, length); fflush(dest); if (length) usleep(length*1000L); } int main(int argc, char **argv) { int rank, size, partner; /* MPI accessories */ int i=0,j=0,k=0,l=0,n,node; /* generic counters */ int check=0, sync=0; /* program flags */ note n0[NOTE_BUF], n1[NOTE_BUF], n2[NOTE_BUF], n3[NOTE_BUF]; FILE *infile; MPI_Status stat; int Len0=0, Len1=0, Len2=0, Len3=0; MPI_Init(&argc, &argv); /* size = query number of processes */ Page 4 MPI_Comm_size(MPI_COMM_WORLD, &size); /* rank = which one am I? */ MPI_Comm_rank(MPI_COMM_WORLD, &rank); dest = fopen("/dev/console", "w"); if (!dest) { perror ("Destination open error"); exit(1); } /* world node job description */ if (rank == 0) { /* read file and distribute parts to array */ infile = fopen("obeautf.dat","r"); while(!feof(infile)) { fscanf(infile,"%d",&node); switch(node) { case(1): fscanf(infile,"%d",&n0[i].freq); fscanf(infile,"%d",&n0[i].length); Len0 += n0[i].length; i++; break; case(2): fscanf(infile,"%d",&n1[j].freq); fscanf(infile,"%d",&n1[j].length); Len1 += n1[j].length; j++; break; case(3): fscanf(infile,"%d",&n2[k].freq); fscanf(infile,"%d",&n2[k].length); Len2 += n2[k].length; k++; break; case(4): fscanf(infile,"%d",&n3[l].freq); fscanf(infile,"%d",&n3[l].length); Len3 += n3[l].length; l++; break; } } if (!(Len0 + Len0 == Len0 + Len0)) { printf("L0=%d i=%d L1=%d j=%d L2=%d k=%d L3=%d l=%d\n", Len0, i, Len1, j, Len2,k ,Len3, l); printf("\n Error: Length of times different, cannot synchronize.\n"); exit(1); } /* send array size and array to individual nodes */ MPI_Send(&j, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); MPI_Send(n1, 2*j, MPI_INT, 1, 0, MPI_COMM_WORLD); MPI_Send(&k, 1, MPI_INT, 2, 0, MPI_COMM_WORLD); MPI_Send(n2, 2*k, MPI_INT, 2, 0, MPI_COMM_WORLD); MPI_Send(&l, 1, MPI_INT, 3, 0, MPI_COMM_WORLD); MPI_Send(n3, 2*l, MPI_INT, 3, 0, MPI_COMM_WORLD); /* checks if all nodes are present & synchronize */ printf("\n Piece will be synchronized at every %d ms\n", MILEPOST); printf(" n%d querying ...\n", rank); for ( partner=1 ; partner<size ; partner++ ) { MPI_Recv(&check, 1, MPI_INT, partner, 0, MPI_COMM_WORLD, &stat); if (check==1) Page 5 printf(" n%d synchronized ...\n", partner); else printf(" n%d MIA\n", partner); } /* play sound */ for (n=0; n<i; n++) { sound(n0[n].freq,n0[n].length*TEMPO); sync += n0[n].length; if (sync >= MILEPOST) { check = 1; for ( partner=1 ; partner<size ; partner++ ) { MPI_Recv(&sync, 1, MPI_INT, partner, 0, MPI_COMM_WORLD, &stat); printf(" rcved from n%d\n", partner); } for ( partner=1 ; partner<size ; partner++ ) MPI_Send(&check, 1, MPI_INT, partner, 0, MPI_COMM_WORLD); printf(" === re-sync-ed ===\n\n"); sync = 0; } printf(" Completed %d\%, sync = %d/%d\n", n*100/i, sync, MILEPOST); } printf(" Completed 100\%\n"); } /* other nodes: */ else { /* receive size of array and array from world node */ MPI_Recv(&i, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &stat); MPI_Recv(n1, 2*i, MPI_INT, 0, 0, MPI_COMM_WORLD, &stat); check = 1; /* alert world node, init sync */ MPI_Send(&check, 1, MPI_INT, 0, 0, MPI_COMM_WORLD); for (n=0; n<i; n++) { sound(n1[n].freq,n1[n].length*TEMPO); sync += n1[n].length; /* synchronizing routine, performed at each MILEPOST */ if (sync >= MILEPOST) { MPI_Send(&sync, 1, MPI_INT, 0, 0, MPI_COMM_WORLD); MPI_Recv(&check, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &stat); sync = 0; } } } /* All Done */ MPI_Finalize(); exit(0); } Page 6 APPENDIX B COMPILING & EXECUTING THE PROGRAM Playing Sounds to All the Nodes Because of the way the sound function was written, it plays to the local speaker even though you executed the file on a remote machine. The destination it writes to is stdout. In order to fix this, the /dev/console file has to accessed in order to play music on a remote computer. To do this (special thanks to fellow colleague Nilesh Bhide), one has to change the ownership of the executable to root and assign the sticky bit to it. Explicit command lines are shown below. Compiling & Executing The steps to compile and run the program are presented below: # # # # # # # # # hcc -o music music_v4.c –lmpi sudo /bin/tcsh chown root:root music chmod +s music scp music 10.0.0.2:/home/lyap1/LAM/PROJ6 scp music 10.0.0.3:/home/lyap1/LAM/PROJ6 scp music 10.0.0.4:/home/lyap1/LAM/PROJ6 exit mpirun -v n0-3 music Page 7 Compile program Switch to root Change ownership of music to root Assign sticky bit to music Copies executable file to other nodes Revert from root to user Run executable