module Bones # This class holds the main functionality: the Bones source- # to-source compilation engine based on algorithmic skeletons. # This class processes command line arguments, makes calls to # the Bones preprocessor and the CAST gem, analyzes the source # code, performs source transformations, instantiates the # skeletons, and finally writes output code to file. class Engine < Common # Locate the skeletons directory. BONES_DIR_SKELETONS = File.join(BONES_DIR,'skeletons') # Set the name of the transformations file as found in the skeleton library. SKELETON_FILE = 'skeletons.txt' # A list of timer files to be found in the skeleton library. TIMER_FILES = ['timer_1_start','timer_1_stop','timer_2_start','timer_2_stop'] # A list of files to be found in the common directory of the skeleton library (excluding timer files). COMMON_FILES = ['prologue','epilogue','mem_prologue','mem_copy_H2D','mem_copy_D2H','mem_epilogue'] # The name of the file containing the globals as found in the skeleton library COMMON_GLOBALS = 'globals' # The name of the file containing the header file for the original C code as found in the skeleton library COMMON_HEADER = 'header' # The name of the file containing the globals for the kernel files as found in the skeleton library COMMON_GLOBALS_KERNEL = 'globals_kernel' # The extension of a host file in the skeleton library. See also SKELETON_DEVICE. SKELETON_HOST = '.host' # The extension of a device file in the skeleton library. See also SKELETON_HOST. SKELETON_DEVICE = '.kernel' # The suffix added to the generated output file for the host file. See also OUTPUT_DEVICE. OUTPUT_HOST = '_host' # The suffix added to the generated output file for the device file. See also OUTPUT_HOST. OUTPUT_DEVICE = '_device' # The suffix added to the generated verification file. See also OUTPUT_DEVICE and OUTPUT_HOST. OUTPUT_VERIFICATION = '_verification' # Initializes the engine and processes the command line # arguments. This method uses the 'trollop' gem to parse # the arguments and to create a nicely formatted help menu. # This method additionally initializes a result-hash and # reads the contents of the source file from disk. # # ==== Command-line usage: # bones --application --target [OPTIONS] # # ==== Options: # --application, -a : Input application file # --target, -t : Target processor (choose from: 'GPU-CUDA','GPU-OPENCL-AMD','CPU-OPENCL-INTEL','CPU-OPENCL-AMD','CPU-OPENMP','CPU-C') # --measurements, -m: Enable/disable timers # --version, -v: Print version and exit # --help, -h: Show this message # def initialize @result = {:original_code => [], :header_code => [], :host_declarations => [], :host_code_lists => [], :algorithm_declarations => [], :algorithm_code_lists => [], :verify_code => []} # Provides a list of possible targets (e.g. GPU-CUDA, 'CPU-OPENCL-INTEL'). targets = [] Dir[File.join(BONES_DIR_SKELETONS,'*')].each do |entry| if (File.directory?(entry)) && !(entry =~ /verification/) targets.push(File.basename(entry)) end end targets = targets.sort # Parse the command line options using the 'trollop' gem. pp_targets = targets.inspect.gsub(/("|\[)|\]/,'') @options = Trollop::options do version 'Bones '+File.read(BONES_DIR+'/VERSION').strip+' (c) 2012 Cedric Nugteren, Eindhoven University of Technology' banner NL+'Bones is a parallelizing source-to-source compiler based on algorithmic skeletons. ' + 'For more information, see the README.rdoc file or visit the Bones website at http://parse.ele.tue.nl/bones/.' + NL + NL + 'Usage:' + NL + ' bones --application --target [OPTIONS]' + NL + 'using the following flags:' opt :application, 'Input application file', :short => 'a', :type => String opt :target, 'Target processor (choose from: '+pp_targets+')', :short => 't', :type => String opt :measurements, 'Enable/disable timers', :short => 'm', :default => false opt :verify, 'Verify correctness of the generated code', :short => 'c', :default => false opt :only_alg_number, 'Only generate code for the x-th species (99 -> all)', :short => 'o', :type => Integer, :default => 99 opt :merge_factor, 'Thread merge factor, default is 1 (==disabled)', :short => 'f', :type => Integer, :default => 1 end Trollop::die 'no input file supplied (use: --application)' if !@options[:application_given] Trollop::die 'no target supplied (use: --target)' if !@options[:target_given] Trollop::die 'input file "'+@options[:application]+'"does not exist ' if !File.exists?(@options[:application]) Trollop::die 'target not supported, supported targets are: '+pp_targets if !targets.include?(@options[:target].upcase) @options[:name] = @options[:application].split('/').last.split('.').first @options[:target] = @options[:target].upcase # Extension for the host files corresponding to the target. @extension = File.extname(Dir[File.join(BONES_DIR_SKELETONS,@options[:target],'common','*')][0]) # Extension for the device files corresponding to the target. @algorithm_extension = File.extname(Dir[File.join(BONES_DIR_SKELETONS,@options[:target],'kernel','*.kernel.*')][0]) # Set a prefix for functions called from the original file but defined in a host file @prefix = (@options[:target] == 'GPU-CUDA') ? '' : '' # Set the location for the skeleton library @dir = {} @dir[:library] = File.join(BONES_DIR_SKELETONS,@options[:target]) @dir[:skeleton_library] = File.join(@dir[:library],'kernel') @dir[:common_library] = File.join(@dir[:library],'common') @dir[:verify_library] = File.join(BONES_DIR_SKELETONS,'verification') # Obtain the source code from file @source = File.open(@options[:application],'r'){|f| f.read} @basename = File.basename(@options[:application],'.c') end # Method to process a file and to output target code. This # method calls all relevant private methods. # # ==== Tasks: # * Run the preprocessor to obtain algorithm information. # * Use the 'CAST' gem to parse the source into an AST. # * Call the code generator to perform the real work and produce output. def process # Run the preprocessor preprocessor = Bones::Preprocessor.new(@source,File.dirname(@options[:application]),@basename) preprocessor.process @result[:header_code] = preprocessor.header_code @result[:device_header] = preprocessor.device_header @result[:header_code] += '#include '+NL if @options[:measurements] # Parse the source code into AST parser = C::Parser.new parser.type_names << 'FILE' parser.type_names << 'size_t' ast = parser.parse(preprocessor.target_code) ast.preprocess # Set the algorithm's skeleton and generate the global code one_time = true preprocessor.algorithms.each_with_index do |algorithm,algorithm_number| algorithm.species.set_skeleton(File.join(@dir[:library],SKELETON_FILE)) if algorithm.species.skeleton_name && one_time @result[:host_code_lists].push(File.read(File.join(@dir[:common_library],COMMON_GLOBALS+@extension))) @result[:algorithm_code_lists].push(File.read(File.join(@dir[:common_library],COMMON_GLOBALS_KERNEL+@extension))) one_time = false end end # Perform code generation @result[:original_code] = ast preprocessor.algorithms.each_with_index do |algorithm,algorithm_number| if @options[:only_alg_number] == 99 || algorithm_number == [@options[:only_alg_number],preprocessor.algorithms.length-1].min puts MESSAGE+'Starting code generation for algorithm "'+algorithm.name+'"' if algorithm.species.skeleton_name algorithm.merge_factor = @options[:merge_factor] if (@options[:target] == 'GPU-CUDA') algorithm.set_function(ast) algorithm.populate_variables(ast,preprocessor.defines) algorithm.populate_lists() algorithm.populate_hash() generate(algorithm) puts MESSAGE+'Code generated using the "'+algorithm.species.skeleton_name+'" skeleton' else puts WARNING+'Skeleton "'+algorithm.species.name+'" not available' end end end end # This method writes the output code to files. It creates # a new directory formatted as 'name_target' and produces # three files. # # ==== Output files: # * +main+ - a file containing the original code with function calls substituting the original algorithms. # * +target+ - a file containing the host code for the target. # * +kernel+ - a file containing the kernel code for the target. def write_output # Create a new directory for the output directory = @options[:application].split('.').first+'_'+@options[:target] Dir.mkdir(directory,0744) unless File.directory?(directory) parser = C::Parser.new parser.type_names << 'FILE' parser.type_names << 'size_t' # Populate the main file File.open(File.join(directory,@options[:application].split(File::SEPARATOR).last),'w') do |main| main.puts '#include ' if @options[:verify] main.puts @result[:header_code] main.puts File.read(File.join(@dir[:common_library],COMMON_HEADER+@extension)) main.puts @result[:host_declarations] main.puts begin main.puts parser.parse(@result[:original_code]).to_s rescue puts WARNING+'Recovering from CAST parse error' main.puts parser.parse(@result[:original_code].clone).to_s end end # Populate the verification file4 if @options[:verify] File.open(File.join(directory,@options[:name]+OUTPUT_VERIFICATION+@extension),'w') do |verification| verification.puts @result[:header_code] verification.puts File.read(File.join(@dir[:verify_library],'header.c')) verification.puts verification.puts @result[:verify_code] end end # Populate the target file File.open(File.join(directory,@options[:name]+OUTPUT_HOST+@extension),'w') do |target| target.puts @result[:header_code] target.puts @result[:algorithm_declarations] target.puts target.puts @result[:host_code_lists] end # Populate the algorithm file File.open(File.join(directory,@options[:name]+OUTPUT_DEVICE+@algorithm_extension),'w') do |algorithm| algorithm.puts @result[:device_header] algorithm.puts @result[:algorithm_code_lists] end end # Start of the class's private methods. private # This method takes as an input an indivual algorithm and # generates the corresponding output code. The method first # creates a search-and-replace hash, after which it instan- # tiates a skeleton. # # This method returns a message informing the user whether # the code was succesfully generated or the skeleton was # not available. def generate(algorithm) # Determine the skeleton filenames and load them skeletons from the skeleton library file_name_host = File.join(@dir[:skeleton_library],algorithm.species.skeleton_name+SKELETON_HOST) file_name_device = File.join(@dir[:skeleton_library],algorithm.species.skeleton_name+SKELETON_DEVICE) if !File.exists?(file_name_host+@extension) || !File.exists?(file_name_device+@algorithm_extension) raise_error('Skeleton files for skeleton "'+algorithm.species.skeleton_name+'" not available') end skeletons = {:host => File.read(file_name_host+@extension), :device => File.read(file_name_device+@algorithm_extension)} # Perform the transformations on the algorithm's code algorithm.perform_transformations(algorithm.species.settings) # Load the common skeletons from the skeleton library COMMON_FILES.each do |skeleton| skeletons[skeleton.to_sym] = File.read(File.join(@dir[:common_library],skeleton+@extension)) end # Load the timer code from the skeleton library (only if the '--measurements' flag is given) TIMER_FILES.each do |skeleton| skeletons[skeleton.to_sym] = @options[:measurements] ? File.read(File.join(@dir[:common_library],skeleton+@extension)) : '' end # Perform search-and-replace on the device skeleton search_and_replace!(algorithm.hash,skeletons[:device]) skeletons[:device].remove_extras # Replace mathematical functions with their equivalent device functions if @options[:target] == 'GPU-CUDA' math_functions = {:sqrt => 'sqrtf', :max => 'fmaxf', :min => 'fminf'} math_functions.each do |original, replacement| skeletons[:device].gsub!(/\b#{original}\(/,replacement+'(') end end # Create the algorithm declaration list from the header supplied in the skeletons algorithm_declaration = skeletons[:device].scan(/#{START_DEFINITION}(.+)#{END_DEFINITION}/m).join.strip.remove_extras @result[:algorithm_declarations].push(algorithm_declaration) # Remove the (commented) algorithm declaration from the code and push the skeleton to the output @result[:algorithm_code_lists].push(skeletons[:device].gsub!(/#{START_DEFINITION}(.+)#{END_DEFINITION}/m,'')) # Setup some variables to create the host body function including memory allocation and memory copies processed = {:mem_prologue => '', :mem_copy_H2D => '', :mem_copy_D2H => '', :mem_epilogue => ''} counter = {:out => 0, :in => 0} # Iterate over all the array variables and create a mini-search-and-replace hash for each array (all arrays) algorithm.arrays.each_with_index do |array, arrayid| minihash = { :array => array.name, :type => array.type_name, :flatten => array.flatten, :variable_dimensions => array.size.join('*')} # Apply the mini-search-and-replace hash to create the memory allocations, memory copies (if input only), etc. processed[:mem_prologue] += search_and_replace(minihash,skeletons[:mem_prologue]) processed[:mem_copy_H2D] += search_and_replace(minihash,skeletons[:mem_copy_H2D]) if array.input? || array.species.shared? processed[:mem_epilogue] += search_and_replace(minihash,skeletons[:mem_epilogue]) end # Iterate over all the array variables and create a mini-search-and-replace hash for each array (output arrays) algorithm.arrays.select(OUTPUT).each_with_index do |array, num_array| hash = algorithm.hash["out#{num_array}".to_sym] minihash = { :array => array.name, :type => array.type_name, :flatten => array.flatten, :offset => '('+hash[:dimension0][:from]+')', :variable_dimensions => '('+hash[:dimensions]+')'} # Perform selective copy for arrays with 2 dimensions (uses a for-loop over the memory copies) if array.dimensions == 2 && @options[:target] == 'GPU-CUDA' && false x_from = '('+hash[:dimension0][:from]+')' x_to = '('+hash[:dimension0][:to]+')' x_sum = '('+hash[:dimension0][:sum]+')' x_size = array.size[0] y_from = '('+hash[:dimension1][:from]+')' y_to = '('+hash[:dimension1][:to]+')' y_sum = '('+hash[:dimension1][:sum]+')' y_size = array.size[1] processed[:mem_copy_D2H] += NL+INDENT+"for(int bones_x=#{x_from}; bones_x<=#{x_to}; bones_x++) {"+INDENT*2 minihash[:offset] = "(bones_x*#{y_size})+#{y_from}" minihash[:variable_dimensions] = "#{y_sum}" # Don't do selective copy for multi-dimensional arrays (yet) elsif array.dimensions > 1 minihash[:offset] = '0' minihash[:variable_dimensions] = array.size.join('*') end # Apply the mini-search-and-replace hash to create the memory copies from device to host processed[:mem_copy_D2H] += search_and_replace(minihash,skeletons[:mem_copy_D2H]) if array.dimensions == 2 && @options[:target] == 'GPU-CUDA' && false processed[:mem_copy_D2H] += INDENT+'}' end end # Apply the search-and-replace hash to all timer skeletons and the host skeleton (['host']+TIMER_FILES).each do |skeleton| search_and_replace!(algorithm.hash,skeletons[skeleton.to_sym]) end # Repair some invalid syntax that could have been introduced by performing the search-and-replace skeletons[:host].remove_extras # Run the prologue/epilogue code through the search-and-replace hash search_and_replace!(algorithm.hash,skeletons[:prologue]) search_and_replace!(algorithm.hash,skeletons[:epilogue]) # Construct the final host function, inluding the timers and memory copies host = skeletons[:prologue ] + skeletons[:timer_1_start] + processed[:mem_prologue ] + processed[:mem_copy_H2D ] + skeletons[:timer_2_start] + skeletons[:host ] + skeletons[:timer_2_stop ] + processed[:mem_copy_D2H ] + processed[:mem_epilogue ] + skeletons[:timer_1_stop ] + skeletons[:epilogue ] # Generate code to replace the original code, including verification code if specified by the option flag verify_skeleton = File.read(File.join(@dir[:verify_library],'verify_results.c')) timer_start = (@options[:measurements]) ? File.read(File.join(@dir[:verify_library],'timer_start.c')) : '' timer_stop = (@options[:measurements]) ? File.read(File.join(@dir[:verify_library],'timer_stop.c')) : '' replacement_code, original_definition, verify_definition = algorithm.generate_replacement_code(@options, verify_skeleton, @result[:verify_code], @prefix, timer_start, timer_stop) @result[:host_declarations].push(verify_definition) # Add a performance model to the original code #replacement_code.insert(0,algorithm.performance_model_code('model')) # Replace mallocs and frees in the original code with aligned memory allocations (only for CPU-OpenCL targets) if @options[:target] == 'CPU-OPENCL-INTEL' @result[:original_code].seach_and_replace_function_call(C::Variable.parse('malloc'),C::Variable.parse(VARIABLE_PREFIX+'malloc_128')) @result[:original_code].seach_and_replace_function_call(C::Variable.parse('free'),C::Variable.parse(VARIABLE_PREFIX+'free_128')) end # Give the original main function a new name @result[:original_code].seach_and_replace_function_definition('main',VARIABLE_PREFIX+'main') # Replace the original code with a function call to the newly generated code @result[:original_code].seach_and_replace_node(algorithm.code,replacement_code) # The host code is generated, push the data to the output hashes accelerated_definition = 'void '+algorithm.name+'_accelerated('+algorithm.lists[:host_definition]+')' @result[:host_code_lists].push(@prefix+accelerated_definition+' {'+NL+host+NL+'}') @result[:host_declarations].push(@prefix+accelerated_definition+';'+NL+@prefix+original_definition+';') end end end