From 5f30133ce5c6237ce0547e4b13b81698a3f9fb5a Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 3 Sep 2024 16:25:53 -0400 Subject: [PATCH 001/162] use vector of vectors instead of int** --- vpr/src/place/place_macro.cpp | 46 +++++++++----------- vpr/src/place/place_macro.h | 3 ++ vpr/src/util/vpr_utils.cpp | 82 +++++++++++++++++++++++++---------- vpr/src/util/vpr_utils.h | 5 ++- 4 files changed, 85 insertions(+), 51 deletions(-) diff --git a/vpr/src/place/place_macro.cpp b/vpr/src/place/place_macro.cpp index 69c06503ca7..c4526c3eac0 100644 --- a/vpr/src/place/place_macro.cpp +++ b/vpr/src/place/place_macro.cpp @@ -1,5 +1,7 @@ + +#include "place_macro.h" + #include -#include #include #include #include @@ -7,14 +9,10 @@ #include "vtr_assert.h" #include "vtr_memory.h" #include "vtr_util.h" - #include "vpr_types.h" #include "vpr_error.h" #include "physical_types.h" #include "globals.h" -#include "place.h" -#include "read_xml_arch_file.h" -#include "place_macro.h" #include "vpr_utils.h" #include "echo_files.h" @@ -23,16 +21,16 @@ /* f_idirect_from_blk_pin array allow us to quickly find pins that could be in a * * direct connection. Values stored is the index of the possible direct connection * * as specified in the arch file, OPEN (-1) is stored for pins that could not be * - * part of a direct chain conneciton. * + * part of a direct chain connection. * * [0...device_ctx.num_block_types-1][0...num_pins-1] */ -static int** f_idirect_from_blk_pin = nullptr; +static std::vector> f_idirect_from_blk_pin; /* f_direct_type_from_blk_pin array stores the value SOURCE if the pin is the * * from_pin, SINK if the pin is the to_pin in the direct connection as specified in * * the arch file, OPEN (-1) is stored for pins that could not be part of a direct * * chain conneciton. * * [0...device_ctx.num_block_types-1][0...num_pins-1] */ -static int** f_direct_type_from_blk_pin = nullptr; +static std::vector> f_direct_type_from_blk_pin; /* f_imacro_from_blk_pin maps a blk_num to the corresponding macro index. * * If the block is not part of a macro, the value OPEN (-1) is stored. * @@ -41,7 +39,11 @@ static vtr::vector_map f_imacro_from_iblk; /******************** Subroutine declarations ********************************/ -static void find_all_the_macro(int* num_of_macro, std::vector& pl_macro_member_blk_num_of_this_blk, std::vector& pl_macro_idirect, std::vector& pl_macro_num_members, std::vector>& pl_macro_member_blk_num); +static void find_all_the_macro(int* num_of_macro, + std::vector& pl_macro_member_blk_num_of_this_blk, + std::vector& pl_macro_idirect, + std::vector& pl_macro_num_members, + std::vector>& pl_macro_member_blk_num); static void alloc_and_load_imacro_from_iblk(const std::vector& macros); @@ -56,7 +58,11 @@ static void validate_macros(const std::vector& macros); static bool try_combine_macros(std::vector>& pl_macro_member_blk_num, int matching_macro, int latest_macro); /******************** Subroutine definitions *********************************/ -static void find_all_the_macro(int* num_of_macro, std::vector& pl_macro_member_blk_num_of_this_blk, std::vector& pl_macro_idirect, std::vector& pl_macro_num_members, std::vector>& pl_macro_member_blk_num) { +static void find_all_the_macro(int* num_of_macro, + std::vector& pl_macro_member_blk_num_of_this_blk, + std::vector& pl_macro_idirect, + std::vector& pl_macro_num_members, + std::vector>& pl_macro_member_blk_num) { /* Compute required size: * * Go through all the pins with possible direct connections in * * f_idirect_from_blk_pin. Count the number of heads (which is the same * @@ -331,7 +337,7 @@ std::vector alloc_and_load_placement_macros(t_direct_inf* directs, i /* Sets up the required variables. */ alloc_and_load_idirect_from_blk_pin(directs, num_directs, - &f_idirect_from_blk_pin, &f_direct_type_from_blk_pin); + f_idirect_from_blk_pin, f_direct_type_from_blk_pin); /* Compute required size: * * Go through all the pins with possible direct connections in * @@ -377,7 +383,7 @@ void get_imacro_from_iblk(int* imacro, ClusterBlockId iblk, const std::vector& macros) { diff --git a/vpr/src/place/place_macro.h b/vpr/src/place/place_macro.h index f0707663091..5fa7ed30ca8 100644 --- a/vpr/src/place/place_macro.h +++ b/vpr/src/place/place_macro.h @@ -161,8 +161,11 @@ struct t_pl_macro { /* These are the function declarations. */ std::vector alloc_and_load_placement_macros(t_direct_inf* directs, int num_directs); + void get_imacro_from_iblk(int* imacro, ClusterBlockId iblk, const std::vector& macros); + void set_imacro_for_iblk(int* imacro, ClusterBlockId iblk); + void free_placement_macros_structs(); #endif diff --git a/vpr/src/util/vpr_utils.cpp b/vpr/src/util/vpr_utils.cpp index 0285a42a5da..b3f33f53541 100644 --- a/vpr/src/util/vpr_utils.cpp +++ b/vpr/src/util/vpr_utils.cpp @@ -55,13 +55,32 @@ static void alloc_and_load_blk_pin_from_port_pin(); * Then, check that whether start_pin_index and end_pin_index are specified. If * * they are, mark down the pins from start_pin_index to end_pin_index, inclusive. * * Otherwise, mark down all the pins in that port. */ -static void mark_direct_of_ports(int idirect, int direct_type, char* pb_type_name, char* port_name, int end_pin_index, int start_pin_index, char* src_string, int line, int** idirect_from_blk_pin, int** direct_type_from_blk_pin); +static void mark_direct_of_ports(int idirect, + int direct_type, + char* pb_type_name, + char* port_name, + int end_pin_index, + int start_pin_index, + char* src_string, + int line, + std::vector>& idirect_from_blk_pin, + std::vector>& direct_type_from_blk_pin); static void get_blk_pin_from_port_pin(int blk_type_index, int sub_tile, int port, int port_pin, int* blk_pin); /* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * * direct_type_from_blk_pin with direct_type from start_pin_index to * * end_pin_index. */ -static void mark_direct_of_pins(int start_pin_index, int end_pin_index, int itype, int isub_tile, int iport, int** idirect_from_blk_pin, int idirect, int** direct_type_from_blk_pin, int direct_type, int line, char* src_string); +static void mark_direct_of_pins(int start_pin_index, + int end_pin_index, + int itype, + int isub_tile, + int iport, + std::vector>& idirect_from_blk_pin, + int idirect, + std::vector>& direct_type_from_blk_pin, + int direct_type, + int line, + char* src_string); static void load_pb_graph_pin_lookup_from_index_rec(t_pb_graph_pin** pb_graph_pin_lookup_from_index, t_pb_graph_node* pb_graph_node); @@ -1762,7 +1781,17 @@ void parse_direct_pin_name(char* src_string, int line, int* start_pin_index, int } } -static void mark_direct_of_pins(int start_pin_index, int end_pin_index, int itype, int isub_tile, int iport, int** idirect_from_blk_pin, int idirect, int** direct_type_from_blk_pin, int direct_type, int line, char* src_string) { +static void mark_direct_of_pins(int start_pin_index, + int end_pin_index, + int itype, + int isub_tile, + int iport, + std::vector>& idirect_from_blk_pin, + int idirect, + std::vector>& direct_type_from_blk_pin, + int direct_type, + int line, + char* src_string) { /* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * * direct_type_from_blk_pin with direct_type from start_pin_index to * * end_pin_index. */ @@ -1802,7 +1831,16 @@ static void mark_direct_of_pins(int start_pin_index, int end_pin_index, int ityp } // Finish marking all the pins } -static void mark_direct_of_ports(int idirect, int direct_type, char* pb_type_name, char* port_name, int end_pin_index, int start_pin_index, char* src_string, int line, int** idirect_from_blk_pin, int** direct_type_from_blk_pin) { +static void mark_direct_of_ports(int idirect, + int direct_type, + char* pb_type_name, + char* port_name, + int end_pin_index, + int start_pin_index, + char* src_string, + int line, + std::vector>& idirect_from_blk_pin, + std::vector>& direct_type_from_blk_pin) { /* Go through all the ports in all the blocks to find the port that has the same * * name as port_name and belongs to the block type that has the name pb_type_name. * * Then, check that whether start_pin_index and end_pin_index are specified. If * @@ -1852,7 +1890,10 @@ static void mark_direct_of_ports(int idirect, int direct_type, char* pb_type_nam } // Finish going through all the blocks } -void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, int num_directs, int*** idirect_from_blk_pin, int*** direct_type_from_blk_pin) { +void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, + int num_directs, + std::vector>& idirect_from_blk_pin, + std::vector>& direct_type_from_blk_pin) { /* Allocates and loads idirect_from_blk_pin and direct_type_from_blk_pin arrays. * * * * For a bus (multiple bits) direct connection, all the pins in the bus are marked. * @@ -1860,43 +1901,39 @@ void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, int num_directs, * idirect_from_blk_pin array allow us to quickly find pins that could be in a * * direct connection. Values stored is the index of the possible direct connection * * as specified in the arch file, OPEN (-1) is stored for pins that could not be * - * part of a direct chain conneciton. * + * part of a direct chain connection. * * * * direct_type_from_blk_pin array stores the value SOURCE if the pin is the * * from_pin, SINK if the pin is the to_pin in the direct connection as specified in * * the arch file, OPEN (-1) is stored for pins that could not be part of a direct * - * chain conneciton. * + * chain connection. * * * * Stores the pointers to the two 2D arrays in the addresses passed in. * * * * The two arrays are freed by the caller(s). */ int iblk_pin, idirect, num_type_pins; - int **temp_idirect_from_blk_pin, **temp_direct_type_from_blk_pin; char to_pb_type_name[MAX_STRING_LEN + 1], to_port_name[MAX_STRING_LEN + 1], from_pb_type_name[MAX_STRING_LEN + 1], from_port_name[MAX_STRING_LEN + 1]; + int to_start_pin_index = -1, to_end_pin_index = -1; int from_start_pin_index = -1, from_end_pin_index = -1; auto& device_ctx = g_vpr_ctx.device(); /* Allocate and initialize the values to OPEN (-1). */ - temp_idirect_from_blk_pin = new int*[device_ctx.physical_tile_types.size()]; - temp_direct_type_from_blk_pin = new int*[device_ctx.physical_tile_types.size()]; + idirect_from_blk_pin.resize(device_ctx.physical_tile_types.size()); + direct_type_from_blk_pin.resize(device_ctx.physical_tile_types.size()); for (const auto& type : device_ctx.physical_tile_types) { - if (is_empty_type(&type)) continue; + if (is_empty_type(&type)) { + continue; + } int itype = type.index; num_type_pins = type.num_pins; - temp_idirect_from_blk_pin[itype] = new int[num_type_pins]; - temp_direct_type_from_blk_pin[itype] = new int[num_type_pins]; - - /* Initialize values to OPEN */ - for (iblk_pin = 0; iblk_pin < num_type_pins; iblk_pin++) { - temp_idirect_from_blk_pin[itype][iblk_pin] = OPEN; - temp_direct_type_from_blk_pin[itype][iblk_pin] = OPEN; - } + idirect_from_blk_pin[itype].resize(num_type_pins, OPEN); + direct_type_from_blk_pin[itype].resize(num_type_pins, OPEN); } /* Load the values */ @@ -1919,19 +1956,16 @@ void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, int num_directs, mark_direct_of_ports(idirect, SOURCE, from_pb_type_name, from_port_name, from_end_pin_index, from_start_pin_index, directs[idirect].from_pin, directs[idirect].line, - temp_idirect_from_blk_pin, temp_direct_type_from_blk_pin); + idirect_from_blk_pin, direct_type_from_blk_pin); // Then, find blocks with the same name as to_pb_type_name and from_port_name mark_direct_of_ports(idirect, SINK, to_pb_type_name, to_port_name, to_end_pin_index, to_start_pin_index, directs[idirect].to_pin, directs[idirect].line, - temp_idirect_from_blk_pin, temp_direct_type_from_blk_pin); + idirect_from_blk_pin, direct_type_from_blk_pin); } // Finish going through all the directs - /* Returns the pointer to the 2D arrays by reference. */ - *idirect_from_blk_pin = temp_idirect_from_blk_pin; - *direct_type_from_blk_pin = temp_direct_type_from_blk_pin; } /* diff --git a/vpr/src/util/vpr_utils.h b/vpr/src/util/vpr_utils.h index 24da4489b6b..8b205a019ea 100644 --- a/vpr/src/util/vpr_utils.h +++ b/vpr/src/util/vpr_utils.h @@ -212,7 +212,10 @@ t_pin_range get_pb_pins(t_physical_tile_type_ptr physical_type, float compute_primitive_base_cost(const t_pb_graph_node* primitive); int num_ext_inputs_atom_block(AtomBlockId blk_id); -void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, int num_directs, int*** idirect_from_blk_pin, int*** direct_type_from_blk_pin); +void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, + int num_directs, + std::vector>& idirect_from_blk_pin, + std::vector>& direct_type_from_blk_pin); void parse_direct_pin_name(char* src_string, int line, int* start_pin_index, int* end_pin_index, char* pb_type_name, char* port_name); From e8c5a5c9ac8a69ccba011297b94930a0fe101f02 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 3 Sep 2024 16:59:15 -0400 Subject: [PATCH 002/162] get_imacro_from_iblk return int instead filling an int* argument --- vpr/src/pack/re_cluster_util.cpp | 5 ++- vpr/src/place/analytic_placer.cpp | 3 +- vpr/src/place/initial_placement.cpp | 6 ++-- vpr/src/place/move_utils.cpp | 18 ++++------ vpr/src/place/place_macro.cpp | 47 ++++++++++++--------------- vpr/src/place/place_macro.h | 9 +++-- vpr/src/place/place_timing_update.cpp | 2 +- vpr/src/util/vpr_utils.cpp | 2 +- 8 files changed, 40 insertions(+), 52 deletions(-) diff --git a/vpr/src/pack/re_cluster_util.cpp b/vpr/src/pack/re_cluster_util.cpp index 46eb04955a5..414a0ed5840 100644 --- a/vpr/src/pack/re_cluster_util.cpp +++ b/vpr/src/pack/re_cluster_util.cpp @@ -81,10 +81,9 @@ void commit_mol_move(ClusterBlockId old_clb, //place the new cluster if this function called during placement (after the initial placement is done) if (!during_packing && new_clb_created) { - int imacro; g_vpr_ctx.mutable_placement().mutable_block_locs().resize(g_vpr_ctx.placement().block_locs().size() + 1); - get_imacro_from_iblk(&imacro, old_clb, g_vpr_ctx.placement().pl_macros); - set_imacro_for_iblk(&imacro, new_clb); + int imacro = get_imacro_from_iblk(old_clb, g_vpr_ctx.placement().pl_macros); + set_imacro_for_iblk(imacro, new_clb); place_one_block(new_clb, device_ctx.pad_loc_type, nullptr, nullptr, g_vpr_ctx.mutable_placement().mutable_blk_loc_registry()); } } diff --git a/vpr/src/place/analytic_placer.cpp b/vpr/src/place/analytic_placer.cpp index 4bb632fbdb7..a86f4f24e93 100644 --- a/vpr/src/place/analytic_placer.cpp +++ b/vpr/src/place/analytic_placer.cpp @@ -108,8 +108,7 @@ struct EquationSystem { // returns index in placementCtx.pl_macros, // returns NO_MACRO if blk not in any macros int imacro(ClusterBlockId blk) { - int macro_index; - get_imacro_from_iblk(¯o_index, blk, g_vpr_ctx.mutable_placement().pl_macros); + int macro_index = get_imacro_from_iblk(blk, g_vpr_ctx.mutable_placement().pl_macros); return macro_index; } diff --git a/vpr/src/place/initial_placement.cpp b/vpr/src/place/initial_placement.cpp index 8636de52759..0cf233e07d9 100644 --- a/vpr/src/place/initial_placement.cpp +++ b/vpr/src/place/initial_placement.cpp @@ -1082,8 +1082,7 @@ static void place_all_blocks(const t_placer_opts& placer_opts, //add current block to list to ensure it will be placed sooner in the next iteration in initial placement number_of_unplaced_blks_in_curr_itr++; block_scores[blk_id].failed_to_place_in_prev_attempts++; - int imacro; - get_imacro_from_iblk(&imacro, blk_id, place_ctx.pl_macros); + int imacro = get_imacro_from_iblk(blk_id, place_ctx.pl_macros); if (imacro != -1) { //the block belongs to macro that contain a chain, we need to turn on dense placement in next iteration for that type of block unplaced_blk_type_in_curr_itr.insert(blk_id_type->index); } @@ -1183,8 +1182,7 @@ bool place_one_block(const ClusterBlockId blk_id, bool placed_macro = false; //Lookup to see if the block is part of a macro - int imacro; - get_imacro_from_iblk(&imacro, blk_id, pl_macros); + int imacro = get_imacro_from_iblk(blk_id, pl_macros); if (imacro != -1) { //If the block belongs to a macro, pass that macro to the placement routines VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\tBelongs to a macro %d\n", imacro); diff --git a/vpr/src/place/move_utils.cpp b/vpr/src/place/move_utils.cpp index c40bfce2c02..c7c21cc87a8 100644 --- a/vpr/src/place/move_utils.cpp +++ b/vpr/src/place/move_utils.cpp @@ -87,12 +87,11 @@ e_block_move_result find_affected_blocks(t_pl_blocks_to_be_moved& blocks_affecte const GridBlock& grid_blocks = blk_loc_registry.grid_blocks(); const auto& pl_macros = g_vpr_ctx.placement().pl_macros; - int imacro_from; e_block_move_result outcome = e_block_move_result::VALID; t_pl_loc from = block_locs[b_from].loc; - get_imacro_from_iblk(&imacro_from, b_from, pl_macros); + int imacro_from = get_imacro_from_iblk(b_from, pl_macros); if (imacro_from != -1) { // b_from is part of a macro, I need to swap the whole macro @@ -106,8 +105,7 @@ e_block_move_result find_affected_blocks(t_pl_blocks_to_be_moved& blocks_affecte } else { ClusterBlockId b_to = grid_blocks.block_at_location(to); - int imacro_to = -1; - get_imacro_from_iblk(&imacro_to, b_to, pl_macros); + int imacro_to = get_imacro_from_iblk(b_to, pl_macros); if (imacro_to != -1) { //To block is a macro but from is a single block. @@ -209,8 +207,7 @@ e_block_move_result record_macro_swaps(t_pl_blocks_to_be_moved& blocks_affected, outcome = e_block_move_result::ABORT; } else { ClusterBlockId b_to = grid_blocks.block_at_location(curr_to); - int imacro_to = -1; - get_imacro_from_iblk(&imacro_to, b_to, pl_macros); + int imacro_to = get_imacro_from_iblk(b_to, pl_macros); if (imacro_to != -1) { //To block is a macro @@ -366,8 +363,7 @@ e_block_move_result record_macro_move(t_pl_blocks_to_be_moved& blocks_affected, blocks_affected.record_block_move(member.blk_index, to, blk_loc_registry); - int imacro_to = -1; - get_imacro_from_iblk(&imacro_to, blk_to, pl_macros); + int imacro_to = get_imacro_from_iblk(blk_to, pl_macros); if (blk_to && imacro_to != imacro) { //Block displaced only if exists and not part of current macro displaced_blocks.push_back(blk_to); } @@ -401,8 +397,7 @@ e_block_move_result identify_macro_self_swap_affected_macros(std::vector& m ClusterBlockId blk_to = grid_blocks.block_at_location(to); - int imacro_to = -1; - get_imacro_from_iblk(&imacro_to, blk_to, pl_macros); + int imacro_to = get_imacro_from_iblk(blk_to, pl_macros); if (imacro_to != -1) { auto itr = std::find(macros.begin(), macros.end(), imacro_to); @@ -447,8 +442,7 @@ e_block_move_result record_macro_self_swaps(t_pl_blocks_to_be_moved& blocks_affe } auto is_non_macro_block = [&](ClusterBlockId blk) { - int imacro_blk = -1; - get_imacro_from_iblk(&imacro_blk, blk, pl_macros); + int imacro_blk = get_imacro_from_iblk(blk, pl_macros); if (std::find(affected_macros.begin(), affected_macros.end(), imacro_blk) != affected_macros.end()) { return false; diff --git a/vpr/src/place/place_macro.cpp b/vpr/src/place/place_macro.cpp index c4526c3eac0..35a3c36117b 100644 --- a/vpr/src/place/place_macro.cpp +++ b/vpr/src/place/place_macro.cpp @@ -28,7 +28,7 @@ static std::vector> f_idirect_from_blk_pin; /* f_direct_type_from_blk_pin array stores the value SOURCE if the pin is the * * from_pin, SINK if the pin is the to_pin in the direct connection as specified in * * the arch file, OPEN (-1) is stored for pins that could not be part of a direct * - * chain conneciton. * + * chain connection. * * [0...device_ctx.num_block_types-1][0...num_pins-1] */ static std::vector> f_direct_type_from_blk_pin; @@ -39,11 +39,10 @@ static vtr::vector_map f_imacro_from_iblk; /******************** Subroutine declarations ********************************/ -static void find_all_the_macro(int* num_of_macro, - std::vector& pl_macro_member_blk_num_of_this_blk, - std::vector& pl_macro_idirect, - std::vector& pl_macro_num_members, - std::vector>& pl_macro_member_blk_num); +static int find_all_the_macro(std::vector& pl_macro_member_blk_num_of_this_blk, + std::vector& pl_macro_idirect, + std::vector& pl_macro_num_members, + std::vector>& pl_macro_member_blk_num); static void alloc_and_load_imacro_from_iblk(const std::vector& macros); @@ -58,11 +57,10 @@ static void validate_macros(const std::vector& macros); static bool try_combine_macros(std::vector>& pl_macro_member_blk_num, int matching_macro, int latest_macro); /******************** Subroutine definitions *********************************/ -static void find_all_the_macro(int* num_of_macro, - std::vector& pl_macro_member_blk_num_of_this_blk, - std::vector& pl_macro_idirect, - std::vector& pl_macro_num_members, - std::vector>& pl_macro_member_blk_num) { +static int find_all_the_macro(std::vector& pl_macro_member_blk_num_of_this_blk, + std::vector& pl_macro_idirect, + std::vector& pl_macro_num_members, + std::vector>& pl_macro_member_blk_num) { /* Compute required size: * * Go through all the pins with possible direct connections in * * f_idirect_from_blk_pin. Count the number of heads (which is the same * @@ -194,7 +192,7 @@ static void find_all_the_macro(int* num_of_macro, } // Finish going through all blocks. // Now, all the data is readily stored in the temporary data structures. - *num_of_macro = num_macro; + return num_macro; } static bool try_combine_macros(std::vector>& pl_macro_member_blk_num, int matching_macro, int latest_macro) { @@ -326,7 +324,6 @@ std::vector alloc_and_load_placement_macros(t_direct_inf* directs, i * The placement macro array is freed by the caller(s). */ /* Declaration of local variables */ - int num_macro; auto& cluster_ctx = g_vpr_ctx.clustering(); /* Allocate maximum memory for temporary variables. */ @@ -345,9 +342,8 @@ std::vector alloc_and_load_placement_macros(t_direct_inf* directs, i * as the number macros) and also the length of each macro * * Head - blocks with to_pin OPEN and from_pin connected * * Tail - blocks with to_pin connected and from_pin OPEN */ - num_macro = 0; - find_all_the_macro(&num_macro, pl_macro_member_blk_num_of_this_blk, - pl_macro_idirect, pl_macro_num_members, pl_macro_member_blk_num); + int num_macro = find_all_the_macro(pl_macro_member_blk_num_of_this_blk, + pl_macro_idirect, pl_macro_num_members, pl_macro_member_blk_num); /* Allocate the memories for the macro. */ std::vector macros(num_macro); @@ -375,7 +371,8 @@ std::vector alloc_and_load_placement_macros(t_direct_inf* directs, i return macros; } -void get_imacro_from_iblk(int* imacro, ClusterBlockId iblk, const std::vector& macros) { + +int get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros) { /* This mapping is needed for fast lookup's whether the block with index * * iblk belongs to a placement macro or not. * * * @@ -387,19 +384,22 @@ void get_imacro_from_iblk(int* imacro, ClusterBlockId iblk, const std::vector& macro void free_placement_macros_structs() { /* This function frees up all the static data structures used. */ - - // This frees up the two arrays and set the pointers to NULL - auto& device_ctx = g_vpr_ctx.device(); - unsigned int itype; - vtr::release_memory(f_idirect_from_blk_pin); vtr::release_memory(f_direct_type_from_blk_pin); } diff --git a/vpr/src/place/place_macro.h b/vpr/src/place/place_macro.h index 5fa7ed30ca8..83f19b1c438 100644 --- a/vpr/src/place/place_macro.h +++ b/vpr/src/place/place_macro.h @@ -133,9 +133,12 @@ #ifndef PLACE_MACRO_H #define PLACE_MACRO_H + #include #include "physical_types.h" +#include "clustered_netlist_fwd.h" +#include "vpr_types.h" /* These are the placement macro structure. * It is in the form of array of structs instead of @@ -159,12 +162,12 @@ struct t_pl_macro { std::vector members; }; -/* These are the function declarations. */ + std::vector alloc_and_load_placement_macros(t_direct_inf* directs, int num_directs); -void get_imacro_from_iblk(int* imacro, ClusterBlockId iblk, const std::vector& macros); +int get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros); -void set_imacro_for_iblk(int* imacro, ClusterBlockId iblk); +void set_imacro_for_iblk(int imacro, ClusterBlockId iblk); void free_placement_macros_structs(); diff --git a/vpr/src/place/place_timing_update.cpp b/vpr/src/place/place_timing_update.cpp index 8c941bd1d81..6c7e0997c1e 100644 --- a/vpr/src/place/place_timing_update.cpp +++ b/vpr/src/place/place_timing_update.cpp @@ -298,7 +298,7 @@ void update_td_costs(const PlaceDelayModel* delay_model, #ifdef VTR_ASSERT_DEBUG_ENABLED double check_timing_cost = 0.; - comp_td_costs(delay_model, place_crit, &check_timing_cost); + comp_td_costs(delay_model, place_crit, placer_state, &check_timing_cost); VTR_ASSERT_DEBUG_MSG(check_timing_cost == *timing_cost, "Total timing cost calculated incrementally in update_td_costs() is " "not consistent with value calculated from scratch in comp_td_costs()"); diff --git a/vpr/src/util/vpr_utils.cpp b/vpr/src/util/vpr_utils.cpp index b3f33f53541..18966e881ac 100644 --- a/vpr/src/util/vpr_utils.cpp +++ b/vpr/src/util/vpr_utils.cpp @@ -1912,7 +1912,7 @@ void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, * * * The two arrays are freed by the caller(s). */ - int iblk_pin, idirect, num_type_pins; + int idirect, num_type_pins; char to_pb_type_name[MAX_STRING_LEN + 1], to_port_name[MAX_STRING_LEN + 1], from_pb_type_name[MAX_STRING_LEN + 1], from_port_name[MAX_STRING_LEN + 1]; From 9da4bce5ee8667af83338dc2eab2f37506e8b2c0 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 3 Sep 2024 18:00:30 -0400 Subject: [PATCH 003/162] t_direct_inf* Directs ---> std::vector Directs --- libs/libarchfpga/src/arch_util.cpp | 7 +- libs/libarchfpga/src/echo_arch.cpp | 8 +-- libs/libarchfpga/src/physical_types.h | 9 ++- libs/libarchfpga/src/read_xml_arch_file.cpp | 75 ++++++++++----------- utils/route_diag/src/main.cpp | 1 - vpr/src/base/place_and_route.cpp | 7 +- vpr/src/base/vpr_api.cpp | 6 +- vpr/src/place/place.cpp | 14 ++-- vpr/src/place/place.h | 3 +- vpr/src/place/place_delay_model.cpp | 4 +- vpr/src/place/place_delay_model.h | 3 +- vpr/src/place/place_macro.cpp | 5 +- vpr/src/place/place_macro.h | 2 +- vpr/src/place/timing_place_lookup.cpp | 12 ++-- vpr/src/place/timing_place_lookup.h | 3 +- vpr/src/route/route.cpp | 4 +- vpr/src/route/route.h | 3 +- vpr/src/route/route_utils.cpp | 5 +- vpr/src/route/route_utils.h | 3 +- vpr/src/route/router_delay_profiling.cpp | 5 +- vpr/src/route/router_delay_profiling.h | 3 +- vpr/src/route/rr_graph.cpp | 73 +++++++++----------- vpr/src/route/rr_graph.h | 3 +- vpr/src/util/vpr_utils.cpp | 31 ++++----- vpr/src/util/vpr_utils.h | 5 +- vpr/test/test_connection_router.cpp | 1 - 26 files changed, 120 insertions(+), 175 deletions(-) diff --git a/libs/libarchfpga/src/arch_util.cpp b/libs/libarchfpga/src/arch_util.cpp index e77c70f5ed6..437f879c0bb 100644 --- a/libs/libarchfpga/src/arch_util.cpp +++ b/libs/libarchfpga/src/arch_util.cpp @@ -157,12 +157,7 @@ void free_arch(t_arch* arch) { free_arch_models(arch->models); - for (int i = 0; i < arch->num_directs; ++i) { - vtr::free(arch->Directs[i].name); - vtr::free(arch->Directs[i].from_pin); - vtr::free(arch->Directs[i].to_pin); - } - vtr::free(arch->Directs); + vtr::release_memory(arch->Directs); vtr::free(arch->architecture_id); diff --git a/libs/libarchfpga/src/echo_arch.cpp b/libs/libarchfpga/src/echo_arch.cpp index 777d655c3e6..5b9e89f5ee3 100644 --- a/libs/libarchfpga/src/echo_arch.cpp +++ b/libs/libarchfpga/src/echo_arch.cpp @@ -332,10 +332,10 @@ void PrintArchInfo(FILE* Echo, const t_arch* arch) { //Direct List fprintf(Echo, "*************************************************\n"); fprintf(Echo, "Direct List:\n"); - for (i = 0; i < arch->num_directs; i++) { + for (i = 0; i < (int)arch->Directs.size(); i++) { fprintf(Echo, "\tDirect[%d]: name %s from_pin %s to_pin %s\n", i + 1, - arch->Directs[i].name, arch->Directs[i].from_pin, - arch->Directs[i].to_pin); + arch->Directs[i].name.c_str(), arch->Directs[i].from_pin.c_str(), + arch->Directs[i].to_pin.c_str()); fprintf(Echo, "\t\t\t\t x_offset %d y_offset %d z_offset %d\n", arch->Directs[i].x_offset, arch->Directs[i].y_offset, arch->Directs[i].sub_tile_offset); @@ -347,7 +347,7 @@ void PrintArchInfo(FILE* Echo, const t_arch* arch) { fprintf(Echo, "*************************************************\n"); fprintf(Echo, "NoC Router Connection List:\n"); - for (auto noc_router : arch->noc->router_list) { + for (const auto& noc_router : arch->noc->router_list) { fprintf(Echo, "NoC router %d is connected to:\t", noc_router.id); for (auto noc_conn_id : noc_router.connection_list) { fprintf(Echo, "%d\t", noc_conn_id); diff --git a/libs/libarchfpga/src/physical_types.h b/libs/libarchfpga/src/physical_types.h index 949024ada33..a90f29a754e 100644 --- a/libs/libarchfpga/src/physical_types.h +++ b/libs/libarchfpga/src/physical_types.h @@ -1831,9 +1831,9 @@ struct t_rr_switch_inf { * particular placement macro. * */ struct t_direct_inf { - char* name; - char* from_pin; - char* to_pin; + std::string name; + std::string from_pin; + std::string to_pin; int x_offset; int y_offset; int sub_tile_offset; @@ -2031,8 +2031,7 @@ struct t_arch { std::vector Segments; t_arch_switch_inf* Switches = nullptr; int num_switches; - t_direct_inf* Directs = nullptr; - int num_directs = 0; + std::vector Directs; t_model* models = nullptr; t_model* model_library = nullptr; diff --git a/libs/libarchfpga/src/read_xml_arch_file.cpp b/libs/libarchfpga/src/read_xml_arch_file.cpp index f3a9ffaa683..a6c298b64e5 100644 --- a/libs/libarchfpga/src/read_xml_arch_file.cpp +++ b/libs/libarchfpga/src/read_xml_arch_file.cpp @@ -323,7 +323,12 @@ static void ProcessSwitches(pugi::xml_node Node, const bool timing_enabled, const pugiutil::loc_data& loc_data); static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, const int switch_index, t_arch_switch_inf* Switches, const pugiutil::loc_data& loc_data); -static void ProcessDirects(pugi::xml_node Parent, t_direct_inf** Directs, int* NumDirects, const t_arch_switch_inf* Switches, const int NumSwitches, const pugiutil::loc_data& loc_data); + +static std::vector ProcessDirects(pugi::xml_node Parent, + const t_arch_switch_inf* Switches, + const int NumSwitches, + const pugiutil::loc_data& loc_data); + static void ProcessClockMetalLayers(pugi::xml_node parent, std::unordered_map& metal_layers, pugiutil::loc_data& loc_data); @@ -471,9 +476,7 @@ void XmlReadArch(const char* ArchFile, /* Process directs */ Next = get_single_child(architecture, "directlist", loc_data, ReqOpt::OPTIONAL); if (Next) { - ProcessDirects(Next, &(arch->Directs), &(arch->num_directs), - arch->Switches, arch->num_switches, - loc_data); + arch->Directs = ProcessDirects(Next, arch->Switches, arch->num_switches, loc_data); } /* Process Clock Networks */ @@ -4273,45 +4276,34 @@ static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, co } } -static void ProcessDirects(pugi::xml_node Parent, t_direct_inf** Directs, int* NumDirects, const t_arch_switch_inf* Switches, const int NumSwitches, const pugiutil::loc_data& loc_data) { - int i, j; - const char* direct_name; - const char* from_pin_name; - const char* to_pin_name; - const char* switch_name; - - pugi::xml_node Node; - +static std::vector ProcessDirects(pugi::xml_node Parent, + const t_arch_switch_inf* Switches, + const int NumSwitches, + const pugiutil::loc_data& loc_data) { /* Count the children and check they are direct connections */ expect_only_children(Parent, {"direct"}, loc_data); - *NumDirects = count_children(Parent, "direct", loc_data); - - /* Alloc direct list */ - *Directs = nullptr; - if (*NumDirects > 0) { - *Directs = (t_direct_inf*)vtr::malloc(*NumDirects * sizeof(t_direct_inf)); - memset(*Directs, 0, (*NumDirects * sizeof(t_direct_inf))); - } + int num_directs = count_children(Parent, "direct", loc_data); + std::vector directs(num_directs); /* Load the directs. */ - Node = get_first_child(Parent, "direct", loc_data); - for (i = 0; i < *NumDirects; ++i) { + pugi::xml_node Node = get_first_child(Parent, "direct", loc_data); + for (int i = 0; i < num_directs; ++i) { expect_only_attributes(Node, {"name", "from_pin", "to_pin", "x_offset", "y_offset", "z_offset", "switch_name", "from_side", "to_side"}, loc_data); - direct_name = get_attribute(Node, "name", loc_data).value(); + const char* direct_name = get_attribute(Node, "name", loc_data).value(); /* Check for direct name collisions */ - for (j = 0; j < i; ++j) { - if (0 == strcmp((*Directs)[j].name, direct_name)) { + for (int j = 0; j < i; ++j) { + if (directs[j].name == direct_name) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(Node), "Two directs with the same name '%s' were found.\n", direct_name); } } - (*Directs)[i].name = vtr::strdup(direct_name); + directs[i].name = direct_name; /* Figure out the source pin and sink pin name */ - from_pin_name = get_attribute(Node, "from_pin", loc_data).value(); - to_pin_name = get_attribute(Node, "to_pin", loc_data).value(); + const char* from_pin_name = get_attribute(Node, "from_pin", loc_data).value(); + const char* to_pin_name = get_attribute(Node, "to_pin", loc_data).value(); /* Check that to_pin and the from_pin are not the same */ if (0 == strcmp(to_pin_name, from_pin_name)) { @@ -4319,22 +4311,23 @@ static void ProcessDirects(pugi::xml_node Parent, t_direct_inf** Directs, int* N "The source pin and sink pin are the same: %s.\n", to_pin_name); } - (*Directs)[i].from_pin = vtr::strdup(from_pin_name); - (*Directs)[i].to_pin = vtr::strdup(to_pin_name); + directs[i].from_pin = from_pin_name; + directs[i].to_pin = to_pin_name; - (*Directs)[i].x_offset = get_attribute(Node, "x_offset", loc_data).as_int(0); - (*Directs)[i].y_offset = get_attribute(Node, "y_offset", loc_data).as_int(0); - (*Directs)[i].sub_tile_offset = get_attribute(Node, "z_offset", loc_data).as_int(0); + directs[i].x_offset = get_attribute(Node, "x_offset", loc_data).as_int(0); + directs[i].y_offset = get_attribute(Node, "y_offset", loc_data).as_int(0); + directs[i].sub_tile_offset = get_attribute(Node, "z_offset", loc_data).as_int(0); std::string from_side_str = get_attribute(Node, "from_side", loc_data, ReqOpt::OPTIONAL).value(); - (*Directs)[i].from_side = string_to_side(from_side_str); + directs[i].from_side = string_to_side(from_side_str); std::string to_side_str = get_attribute(Node, "to_side", loc_data, ReqOpt::OPTIONAL).value(); - (*Directs)[i].to_side = string_to_side(to_side_str); + directs[i].to_side = string_to_side(to_side_str); //Set the optional switch type - switch_name = get_attribute(Node, "switch_name", loc_data, ReqOpt::OPTIONAL).as_string(nullptr); + const char* switch_name = get_attribute(Node, "switch_name", loc_data, ReqOpt::OPTIONAL).as_string(nullptr); if (switch_name != nullptr) { //Look-up the user defined switch + int j; for (j = 0; j < NumSwitches; j++) { if (0 == strcmp(switch_name, Switches[j].name.c_str())) { break; //Found the switch @@ -4344,21 +4337,23 @@ static void ProcessDirects(pugi::xml_node Parent, t_direct_inf** Directs, int* N archfpga_throw(loc_data.filename_c_str(), loc_data.line(Node), "Could not find switch named '%s' in switch list.\n", switch_name); } - (*Directs)[i].switch_type = j; //Save the correct switch index + directs[i].switch_type = j; //Save the correct switch index } else { //If not defined, use the delayless switch by default //TODO: find a better way of indicating this. Ideally, we would //specify the delayless switch index here, but it does not appear //to be defined at this point. - (*Directs)[i].switch_type = -1; + directs[i].switch_type = -1; } - (*Directs)[i].line = loc_data.line(Node); + directs[i].line = loc_data.line(Node); /* Should I check that the direct chain offset is not greater than the chip? How? */ /* Get next direct element */ Node = Node.next_sibling(Node.name()); } + + return directs; } static void ProcessClockMetalLayers(pugi::xml_node parent, diff --git a/utils/route_diag/src/main.cpp b/utils/route_diag/src/main.cpp index 47637885363..1c3e7de695f 100644 --- a/utils/route_diag/src/main.cpp +++ b/utils/route_diag/src/main.cpp @@ -334,7 +334,6 @@ int main(int argc, const char **argv) { &vpr_setup.RoutingArch, vpr_setup.Segments, Arch.Directs, - Arch.num_directs, is_flat); if(route_options.profile_source) { diff --git a/vpr/src/base/place_and_route.cpp b/vpr/src/base/place_and_route.cpp index f95ff2b9b00..eb64a4b1959 100644 --- a/vpr/src/base/place_and_route.cpp +++ b/vpr/src/base/place_and_route.cpp @@ -191,7 +191,6 @@ int binary_search_place_and_route(const Netlist<>& placement_net_list, det_routing_arch, segment_inf, arch->Directs, - arch->num_directs, false); } success = route(router_net_list, @@ -204,7 +203,6 @@ int binary_search_place_and_route(const Netlist<>& placement_net_list, delay_calc, arch->Chans, arch->Directs, - arch->num_directs, (attempt_count == 0) ? ScreenUpdatePriority::MAJOR : ScreenUpdatePriority::MINOR, is_flat); @@ -330,7 +328,7 @@ int binary_search_place_and_route(const Netlist<>& placement_net_list, placer_opts.place_chan_width = current; try_place(placement_net_list, placer_opts, annealing_sched, router_opts, analysis_opts, noc_opts, arch->Chans, det_routing_arch, segment_inf, - arch->Directs, arch->num_directs, + arch->Directs, false); } @@ -345,7 +343,6 @@ int binary_search_place_and_route(const Netlist<>& placement_net_list, delay_calc, arch->Chans, arch->Directs, - arch->num_directs, ScreenUpdatePriority::MINOR, is_flat); @@ -387,7 +384,7 @@ int binary_search_place_and_route(const Netlist<>& placement_net_list, det_routing_arch, segment_inf, router_opts, - arch->Directs, arch->num_directs, + arch->Directs, &warnings, is_flat); diff --git a/vpr/src/base/vpr_api.cpp b/vpr/src/base/vpr_api.cpp index 2bc4dd2a5f9..3449c4fe72c 100644 --- a/vpr/src/base/vpr_api.cpp +++ b/vpr/src/base/vpr_api.cpp @@ -812,7 +812,6 @@ void vpr_place(const Netlist<>& net_list, t_vpr_setup& vpr_setup, const t_arch& &vpr_setup.RoutingArch, vpr_setup.Segments, arch.Directs, - arch.num_directs, is_flat); auto& filename_opts = vpr_setup.FileNameOpts; @@ -844,7 +843,7 @@ void vpr_load_placement(t_vpr_setup& vpr_setup, const t_arch& arch) { filename_opts.verify_file_digests, device_ctx.grid); //Ensure placement macros are loaded so that they can be drawn after placement (e.g. during routing) - place_ctx.pl_macros = alloc_and_load_placement_macros(arch.Directs, arch.num_directs); + place_ctx.pl_macros = alloc_and_load_placement_macros(arch.Directs); } RouteStatus vpr_route_flow(const Netlist<>& net_list, @@ -1014,7 +1013,6 @@ RouteStatus vpr_route_fixed_W(const Netlist<>& net_list, delay_calc, arch.Chans, arch.Directs, - arch.num_directs, ScreenUpdatePriority::MAJOR, is_flat); @@ -1114,7 +1112,7 @@ void vpr_create_rr_graph(t_vpr_setup& vpr_setup, const t_arch& arch, int chan_wi det_routing_arch, vpr_setup.Segments, router_opts, - arch.Directs, arch.num_directs, + arch.Directs, &warnings, is_flat); //Initialize drawing, now that we have an RR graph diff --git a/vpr/src/place/place.cpp b/vpr/src/place/place.cpp index 2cd55402d47..cd0877f2ab3 100644 --- a/vpr/src/place/place.cpp +++ b/vpr/src/place/place.cpp @@ -191,8 +191,7 @@ static bool is_cube_bb(const e_place_bounding_box_mode place_bb_mode, static void alloc_and_load_placement_structs(float place_cost_exp, const t_placer_opts& placer_opts, const t_noc_opts& noc_opts, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, PlacerState& placer_state); static void alloc_and_load_try_swap_structs(const bool cube_bb); @@ -359,8 +358,7 @@ void try_place(const Netlist<>& net_list, t_chan_width_dist chan_width_dist, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, bool is_flat) { /* Does almost all the work of placing a circuit. Width_fac gives the * * width of the widest channel. Place_cost_exp says what exponent the * @@ -415,7 +413,6 @@ void try_place(const Netlist<>& net_list, det_routing_arch, segment_inf, directs, - num_directs, is_flat); if (isEchoFileEnabled(E_ECHO_PLACEMENT_DELTA_DELAY_MODEL)) { @@ -440,7 +437,7 @@ void try_place(const Netlist<>& net_list, const auto& p_runtime_ctx = placer_state.runtime(); - alloc_and_load_placement_structs(placer_opts.place_cost_exp, placer_opts, noc_opts, directs, num_directs, placer_state); + alloc_and_load_placement_structs(placer_opts.place_cost_exp, placer_opts, noc_opts, directs, placer_state); set_net_handlers_placer_state(placer_state); std::unique_ptr manual_move_generator = std::make_unique(placer_state); @@ -1833,8 +1830,7 @@ static void invalidate_affected_connections(const t_pl_blocks_to_be_moved& block static void alloc_and_load_placement_structs(float place_cost_exp, const t_placer_opts& placer_opts, const t_noc_opts& noc_opts, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, PlacerState& placer_state) { const auto& device_ctx = g_vpr_ctx.device(); const auto& cluster_ctx = g_vpr_ctx.clustering(); @@ -1907,7 +1903,7 @@ static void alloc_and_load_placement_structs(float place_cost_exp, alloc_and_load_try_swap_structs(place_ctx.cube_bb); - place_ctx.pl_macros = alloc_and_load_placement_macros(directs, num_directs); + place_ctx.pl_macros = alloc_and_load_placement_macros(directs); if (noc_opts.noc) { allocate_and_load_noc_placement_structs(); diff --git a/vpr/src/place/place.h b/vpr/src/place/place.h index dba2f79ab23..138c6cdd05d 100644 --- a/vpr/src/place/place.h +++ b/vpr/src/place/place.h @@ -12,8 +12,7 @@ void try_place(const Netlist<>& net_list, t_chan_width_dist chan_width_dist, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, bool is_flat); #endif diff --git a/vpr/src/place/place_delay_model.cpp b/vpr/src/place/place_delay_model.cpp index ea21d581273..21d292cbac1 100644 --- a/vpr/src/place/place_delay_model.cpp +++ b/vpr/src/place/place_delay_model.cpp @@ -331,8 +331,7 @@ std::unique_ptr alloc_lookups_and_delay_model(const Netlist<>& const t_router_opts& router_opts, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, bool is_flat) { return compute_place_delay_model(placer_opts, router_opts, @@ -341,7 +340,6 @@ std::unique_ptr alloc_lookups_and_delay_model(const Netlist<>& segment_inf, chan_width_dist, directs, - num_directs, is_flat); } diff --git a/vpr/src/place/place_delay_model.h b/vpr/src/place/place_delay_model.h index 5f61b856405..58402e0a2c3 100644 --- a/vpr/src/place/place_delay_model.h +++ b/vpr/src/place/place_delay_model.h @@ -35,8 +35,7 @@ std::unique_ptr alloc_lookups_and_delay_model(const Netlist<>& const t_router_opts& router_opts, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, bool is_flat); ///@brief Returns the delay of one point to point connection. diff --git a/vpr/src/place/place_macro.cpp b/vpr/src/place/place_macro.cpp index 35a3c36117b..2cace4a279d 100644 --- a/vpr/src/place/place_macro.cpp +++ b/vpr/src/place/place_macro.cpp @@ -303,7 +303,7 @@ static bool try_combine_macros(std::vector>& pl_macr return true; } -std::vector alloc_and_load_placement_macros(t_direct_inf* directs, int num_directs) { +std::vector alloc_and_load_placement_macros(const std::vector& directs) { /* This function allocates and loads the macros placement macros * * and returns the total number of macros in 2 steps. * * 1) Allocate temporary data structure for maximum possible * @@ -333,8 +333,7 @@ std::vector alloc_and_load_placement_macros(t_direct_inf* directs, i std::vector pl_macro_member_blk_num_of_this_blk(cluster_ctx.clb_nlist.blocks().size()); /* Sets up the required variables. */ - alloc_and_load_idirect_from_blk_pin(directs, num_directs, - f_idirect_from_blk_pin, f_direct_type_from_blk_pin); + alloc_and_load_idirect_from_blk_pin(directs, f_idirect_from_blk_pin, f_direct_type_from_blk_pin); /* Compute required size: * * Go through all the pins with possible direct connections in * diff --git a/vpr/src/place/place_macro.h b/vpr/src/place/place_macro.h index 83f19b1c438..a33771175ab 100644 --- a/vpr/src/place/place_macro.h +++ b/vpr/src/place/place_macro.h @@ -163,7 +163,7 @@ struct t_pl_macro { }; -std::vector alloc_and_load_placement_macros(t_direct_inf* directs, int num_directs); +std::vector alloc_and_load_placement_macros(const std::vector& directs); int get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros); diff --git a/vpr/src/place/timing_place_lookup.cpp b/vpr/src/place/timing_place_lookup.cpp index c16a0d6dbad..c570890dfa7 100644 --- a/vpr/src/place/timing_place_lookup.cpp +++ b/vpr/src/place/timing_place_lookup.cpp @@ -177,15 +177,13 @@ std::unique_ptr compute_place_delay_model(const t_placer_opts& t_det_routing_arch* det_routing_arch, std::vector& segment_inf, t_chan_width_dist chan_width_dist, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, bool is_flat) { vtr::ScopedStartFinishTimer timer("Computing placement delta delay look-up"); t_chan_width chan_width = setup_chan_width(router_opts, chan_width_dist); - alloc_routing_structs(chan_width, router_opts, det_routing_arch, segment_inf, - directs, num_directs, is_flat); + alloc_routing_structs(chan_width, router_opts, det_routing_arch, segment_inf, directs, is_flat); const RouterLookahead* router_lookahead = get_cached_router_lookahead(*det_routing_arch, router_opts.lookahead_type, @@ -1194,7 +1192,7 @@ void OverrideDelayModel::compute_override_delay_model( //Look at all the direct connections that exist, and add overrides to delay model auto& device_ctx = g_vpr_ctx.device(); - for (int idirect = 0; idirect < device_ctx.arch->num_directs; ++idirect) { + for (int idirect = 0; idirect < (int)device_ctx.arch->Directs.size(); ++idirect) { const t_direct_inf* direct = &device_ctx.arch->Directs[idirect]; InstPort from_port = parse_inst_port(direct->from_pin); @@ -1258,8 +1256,8 @@ void OverrideDelayModel::compute_override_delay_model( sampled_rr_pairs.insert({src_rr, sink_rr}); } - VTR_LOGV_WARN(missing_instances > 0, "Found no delta delay for %d bits of inter-block direct connect '%s' (no instances of this direct found)\n", missing_instances, direct->name); - VTR_LOGV_WARN(missing_paths > 0, "Found no delta delay for %d bits of inter-block direct connect '%s' (no routing path found)\n", missing_paths, direct->name); + VTR_LOGV_WARN(missing_instances > 0, "Found no delta delay for %d bits of inter-block direct connect '%s' (no instances of this direct found)\n", missing_instances, direct->name.c_str()); + VTR_LOGV_WARN(missing_paths > 0, "Found no delta delay for %d bits of inter-block direct connect '%s' (no routing path found)\n", missing_paths, direct->name.c_str()); } } diff --git a/vpr/src/place/timing_place_lookup.h b/vpr/src/place/timing_place_lookup.h index 30e1a8ae01a..865b2f44558 100644 --- a/vpr/src/place/timing_place_lookup.h +++ b/vpr/src/place/timing_place_lookup.h @@ -8,8 +8,7 @@ std::unique_ptr compute_place_delay_model(const t_placer_opts& t_det_routing_arch* det_routing_arch, std::vector& segment_inf, t_chan_width_dist chan_width_dist, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, bool is_flat); std::vector get_best_classes(enum e_pin_type pintype, t_physical_tile_type_ptr type); diff --git a/vpr/src/route/route.cpp b/vpr/src/route/route.cpp index 1816f610f9f..24ee0b5a8cf 100644 --- a/vpr/src/route/route.cpp +++ b/vpr/src/route/route.cpp @@ -22,8 +22,7 @@ bool route(const Netlist<>& net_list, std::shared_ptr timing_info, std::shared_ptr delay_calc, t_chan_width_dist chan_width_dist, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, ScreenUpdatePriority first_iteration_priority, bool is_flat) { auto& device_ctx = g_vpr_ctx.mutable_device(); @@ -59,7 +58,6 @@ bool route(const Netlist<>& net_list, segment_inf, router_opts, directs, - num_directs, &warning_count, is_flat); diff --git a/vpr/src/route/route.h b/vpr/src/route/route.h index cf6efb26311..082a417008e 100644 --- a/vpr/src/route/route.h +++ b/vpr/src/route/route.h @@ -27,7 +27,6 @@ bool route(const Netlist<>& net_list, std::shared_ptr timing_info, std::shared_ptr delay_calc, t_chan_width_dist chan_width_dist, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, ScreenUpdatePriority first_iteration_priority, bool is_flat); diff --git a/vpr/src/route/route_utils.cpp b/vpr/src/route/route_utils.cpp index bb89d89fdee..8b53fc1d876 100644 --- a/vpr/src/route/route_utils.cpp +++ b/vpr/src/route/route_utils.cpp @@ -472,8 +472,7 @@ void try_graph(int width_fac, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, t_chan_width_dist chan_width_dist, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, bool is_flat) { auto& device_ctx = g_vpr_ctx.mutable_device(); @@ -502,7 +501,7 @@ void try_graph(int width_fac, det_routing_arch, segment_inf, router_opts, - directs, num_directs, + directs, &warning_count, is_flat); } diff --git a/vpr/src/route/route_utils.h b/vpr/src/route/route_utils.h index fddad8247dd..54dfee53e3b 100644 --- a/vpr/src/route/route_utils.h +++ b/vpr/src/route/route_utils.h @@ -132,8 +132,7 @@ void try_graph(int width_fac, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, t_chan_width_dist chan_width_dist, - t_direct_inf* directs, - int num_directs, + const std::vector& directs, bool is_flat); /* This routine updates the pres_fac used by the drawing functions */ diff --git a/vpr/src/route/router_delay_profiling.cpp b/vpr/src/route/router_delay_profiling.cpp index 256c4fcb933..5921538e22c 100644 --- a/vpr/src/route/router_delay_profiling.cpp +++ b/vpr/src/route/router_delay_profiling.cpp @@ -249,8 +249,7 @@ void alloc_routing_structs(const t_chan_width& chan_width, const t_router_opts& router_opts, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, bool is_flat) { int warnings; t_graph_type graph_type; @@ -270,7 +269,7 @@ void alloc_routing_structs(const t_chan_width& chan_width, det_routing_arch, segment_inf, router_opts, - directs, num_directs, + directs, &warnings, is_flat); diff --git a/vpr/src/route/router_delay_profiling.h b/vpr/src/route/router_delay_profiling.h index 71753a4cb91..c540434c8f1 100644 --- a/vpr/src/route/router_delay_profiling.h +++ b/vpr/src/route/router_delay_profiling.h @@ -59,8 +59,7 @@ void alloc_routing_structs(const t_chan_width& chan_width, const t_router_opts& router_opts, t_det_routing_arch* det_routing_arch, std::vector& segment_inf, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, bool is_flat); void free_routing_structs(); diff --git a/vpr/src/route/rr_graph.cpp b/vpr/src/route/rr_graph.cpp index a5129dc5cf5..62dd90ad9c8 100644 --- a/vpr/src/route/rr_graph.cpp +++ b/vpr/src/route/rr_graph.cpp @@ -137,8 +137,7 @@ static void build_bidir_rr_opins(RRGraphBuilder& rr_graph_builder, const t_chan_details& chan_details_x, const t_chan_details& chan_details_y, const DeviceGrid& grid, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs, const int num_seg_types); @@ -158,8 +157,7 @@ static void build_unidir_rr_opins(RRGraphBuilder& rr_graph_builder, t_rr_edge_info_set& created_rr_edges, bool* Fc_clipped, const t_unified_to_parallel_seg_index& seg_index_map, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs, const int num_seg_types, int& edge_count); @@ -173,8 +171,7 @@ static int get_opin_direct_connections(RRGraphBuilder& rr_graph_builder, int opin, RRNodeId from_rr_node, t_rr_edge_info_set& rr_edges_to_create, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs); static std::function alloc_and_load_rr_graph(RRGraphBuilder& rr_graph_builder, @@ -202,8 +199,7 @@ static std::function alloc_and_load_rr_graph(RRGraphBuilder const int delayless_switch, const enum e_directionality directionality, bool* Fc_clipped, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs, bool is_global_graph, const enum e_clock_modeling clock_modeling, @@ -531,7 +527,7 @@ static void rr_graph_externals(const std::vector& segment_inf, int wire_to_rr_ipin_switch, enum e_base_cost_type base_cost_type); -static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const t_direct_inf* directs, const int num_directs, const int delayless_switch); +static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const std::vector& directs, const int delayless_switch); static t_seg_details* alloc_and_load_global_route_seg_details(const int global_route_switch, int* num_seg_details = nullptr); @@ -654,8 +650,7 @@ static void build_rr_graph(const t_graph_type graph_type, const float R_minW_pmos, const enum e_base_cost_type base_cost_type, const enum e_clock_modeling clock_modeling, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, int* wire_to_rr_ipin_switch, bool is_flat, int* Warnings); @@ -689,8 +684,7 @@ void create_rr_graph(const t_graph_type graph_type, t_det_routing_arch* det_routing_arch, const std::vector& segment_inf, const t_router_opts& router_opts, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, int* Warnings, bool is_flat) { const auto& device_ctx = g_vpr_ctx.device(); @@ -757,7 +751,7 @@ void create_rr_graph(const t_graph_type graph_type, det_routing_arch->R_minW_pmos, router_opts.base_cost_type, router_opts.clock_modeling, - directs, num_directs, + directs, &det_routing_arch->wire_to_rr_ipin_switch, is_flat, Warnings); @@ -981,8 +975,7 @@ static void build_rr_graph(const t_graph_type graph_type, const float R_minW_pmos, const enum e_base_cost_type base_cost_type, const enum e_clock_modeling clock_modeling, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, int* wire_to_rr_ipin_switch, bool is_flat, int* Warnings) { @@ -1011,8 +1004,8 @@ static void build_rr_graph(const t_graph_type graph_type, const auto& rr_graph = device_ctx.rr_graph; t_clb_to_clb_directs* clb_to_clb_directs = nullptr; - if (num_directs > 0) { - clb_to_clb_directs = alloc_and_load_clb_to_clb_directs(directs, num_directs, delayless_switch); + if (!directs.empty()) { + clb_to_clb_directs = alloc_and_load_clb_to_clb_directs(directs, delayless_switch); } /* START SEG_DETAILS */ @@ -1350,7 +1343,6 @@ static void build_rr_graph(const t_graph_type graph_type, auto update_chan_width = alloc_and_load_rr_graph( device_ctx.rr_graph_builder, - device_ctx.rr_graph_builder.rr_nodes(), device_ctx.rr_graph, segment_inf.size(), segment_inf_x.size(), segment_index_map, @@ -1365,7 +1357,8 @@ static void build_rr_graph(const t_graph_type graph_type, delayless_switch, directionality, &Fc_clipped, - directs, num_directs, clb_to_clb_directs, + directs, + clb_to_clb_directs, is_global_graph, clock_modeling, is_flat); @@ -2043,8 +2036,7 @@ static std::function alloc_and_load_rr_graph(RRGraphBuilder const int delayless_switch, const enum e_directionality directionality, bool* Fc_clipped, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs, bool is_global_graph, const enum e_clock_modeling clock_modeling, @@ -2129,14 +2121,14 @@ static std::function alloc_and_load_rr_graph(RRGraphBuilder opin_to_track_map, Fc_out, rr_edges_to_create, chan_details_x, chan_details_y, grid, - directs, num_directs, clb_to_clb_directs, num_seg_types); + directs, clb_to_clb_directs, num_seg_types); } else { VTR_ASSERT(UNI_DIRECTIONAL == directionality); bool clipped; build_unidir_rr_opins(rr_graph_builder, rr_graph, layer, i, j, side, grid, Fc_out, chan_width, chan_details_x, chan_details_y, Fc_xofs, Fc_yofs, rr_edges_to_create, &clipped, seg_index_map, - directs, num_directs, clb_to_clb_directs, num_seg_types, + directs, clb_to_clb_directs, num_seg_types, rr_edges_before_directs); if (clipped) { *Fc_clipped = true; @@ -2592,8 +2584,7 @@ static void build_bidir_rr_opins(RRGraphBuilder& rr_graph_builder, const t_chan_details& chan_details_x, const t_chan_details& chan_details_y, const DeviceGrid& grid, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs, const int num_seg_types) { //Don't connect pins which are not adjacent to channels around the perimeter @@ -2642,7 +2633,7 @@ static void build_bidir_rr_opins(RRGraphBuilder& rr_graph_builder, /* Add in direct connections */ get_opin_direct_connections(rr_graph_builder, rr_graph, layer, i, j, side, pin_index, node_index, rr_edges_to_create, - directs, num_directs, clb_to_clb_directs); + directs, clb_to_clb_directs); } } @@ -4038,8 +4029,7 @@ static void build_unidir_rr_opins(RRGraphBuilder& rr_graph_builder, t_rr_edge_info_set& rr_edges_to_create, bool* Fc_clipped, const t_unified_to_parallel_seg_index& seg_index_map, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs, const int num_seg_types, int& rr_edge_count) { @@ -4140,7 +4130,7 @@ static void build_unidir_rr_opins(RRGraphBuilder& rr_graph_builder, /* Add in direct connections */ get_opin_direct_connections(rr_graph_builder, rr_graph, layer, i, j, side, pin_index, opin_node_index, rr_edges_to_create, - directs, num_directs, clb_to_clb_directs); + directs, clb_to_clb_directs); } } @@ -4149,8 +4139,7 @@ static void build_unidir_rr_opins(RRGraphBuilder& rr_graph_builder, * This data structure supplements the the info in the "directs" data structure * TODO: The function that does this parsing in placement is poorly done because it lacks generality on heterogeniety, should replace with this one */ -static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const t_direct_inf* directs, const int num_directs, int delayless_switch) { - int i; +static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const std::vector& directs, int delayless_switch) { t_clb_to_clb_directs* clb_to_clb_directs; char *tile_name, *port_name; int start_pin_index, end_pin_index; @@ -4159,12 +4148,13 @@ static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const t_direct_in auto& device_ctx = g_vpr_ctx.device(); + const int num_directs = directs.size(); clb_to_clb_directs = new t_clb_to_clb_directs[num_directs]; tile_name = nullptr; port_name = nullptr; - for (i = 0; i < num_directs; i++) { + for (int i = 0; i < num_directs; i++) { //clb_to_clb_directs[i].from_clb_type; clb_to_clb_directs[i].from_clb_pin_start_index = 0; clb_to_clb_directs[i].from_clb_pin_end_index = 0; @@ -4173,12 +4163,12 @@ static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const t_direct_in clb_to_clb_directs[i].to_clb_pin_end_index = 0; clb_to_clb_directs[i].switch_index = 0; - tile_name = new char[strlen(directs[i].from_pin) + strlen(directs[i].to_pin)]; - port_name = new char[strlen(directs[i].from_pin) + strlen(directs[i].to_pin)]; + tile_name = new char[directs[i].from_pin.length() + directs[i].to_pin.length()]; + port_name = new char[directs[i].from_pin.length() + directs[i].to_pin.length()]; // Load from pins // Parse out the pb_type name, port name, and pin range - parse_direct_pin_name(directs[i].from_pin, directs[i].line, &start_pin_index, &end_pin_index, tile_name, port_name); + parse_direct_pin_name(directs[i].from_pin.c_str(), directs[i].line, &start_pin_index, &end_pin_index, tile_name, port_name); // Figure out which type, port, and pin is used for (const auto& type : device_ctx.physical_tile_types) { @@ -4209,7 +4199,7 @@ static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const t_direct_in // Load to pins // Parse out the pb_type name, port name, and pin range - parse_direct_pin_name(directs[i].to_pin, directs[i].line, &start_pin_index, &end_pin_index, tile_name, port_name); + parse_direct_pin_name(directs[i].to_pin.c_str(), directs[i].line, &start_pin_index, &end_pin_index, tile_name, port_name); // Figure out which type, port, and pin is used for (const auto& type : device_ctx.physical_tile_types) { @@ -4240,7 +4230,7 @@ static t_clb_to_clb_directs* alloc_and_load_clb_to_clb_directs(const t_direct_in if (abs(clb_to_clb_directs[i].from_clb_pin_start_index - clb_to_clb_directs[i].from_clb_pin_end_index) != abs(clb_to_clb_directs[i].to_clb_pin_start_index - clb_to_clb_directs[i].to_clb_pin_end_index)) { vpr_throw(VPR_ERROR_ARCH, get_arch_file_name(), directs[i].line, - "Range mismatch from %s to %s.\n", directs[i].from_pin, directs[i].to_pin); + "Range mismatch from %s to %s.\n", directs[i].from_pin.c_str(), directs[i].to_pin.c_str()); } //Set the switch index @@ -4271,8 +4261,7 @@ static int get_opin_direct_connections(RRGraphBuilder& rr_graph_builder, int opin, RRNodeId from_rr_node, t_rr_edge_info_set& rr_edges_to_create, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, const t_clb_to_clb_directs* clb_to_clb_directs) { auto& device_ctx = g_vpr_ctx.device(); @@ -4287,9 +4276,9 @@ static int get_opin_direct_connections(RRGraphBuilder& rr_graph_builder, } //Capacity location determined by pin number relative to pins per capacity instance - int z, relative_opin; - std::tie(z, relative_opin) = get_capacity_location_from_physical_pin(curr_type, opin); + auto [z, relative_opin] = get_capacity_location_from_physical_pin(curr_type, opin); VTR_ASSERT(z >= 0 && z < curr_type->capacity); + const int num_directs = directs.size(); /* Iterate through all direct connections */ for (int i = 0; i < num_directs; i++) { diff --git a/vpr/src/route/rr_graph.h b/vpr/src/route/rr_graph.h index 7b7423d23f4..b0b3b7d1f00 100644 --- a/vpr/src/route/rr_graph.h +++ b/vpr/src/route/rr_graph.h @@ -27,8 +27,7 @@ void create_rr_graph(const t_graph_type graph_type, t_det_routing_arch* det_routing_arch, const std::vector& segment_inf, const t_router_opts& router_opts, - const t_direct_inf* directs, - const int num_directs, + const std::vector& directs, int* Warnings, bool is_flat); diff --git a/vpr/src/util/vpr_utils.cpp b/vpr/src/util/vpr_utils.cpp index 18966e881ac..731fea110b9 100644 --- a/vpr/src/util/vpr_utils.cpp +++ b/vpr/src/util/vpr_utils.cpp @@ -61,7 +61,7 @@ static void mark_direct_of_ports(int idirect, char* port_name, int end_pin_index, int start_pin_index, - char* src_string, + const char* src_string, int line, std::vector>& idirect_from_blk_pin, std::vector>& direct_type_from_blk_pin); @@ -80,7 +80,7 @@ static void mark_direct_of_pins(int start_pin_index, std::vector>& direct_type_from_blk_pin, int direct_type, int line, - char* src_string); + const char* src_string); static void load_pb_graph_pin_lookup_from_index_rec(t_pb_graph_pin** pb_graph_pin_lookup_from_index, t_pb_graph_node* pb_graph_node); @@ -1701,13 +1701,12 @@ static void alloc_and_load_blk_pin_from_port_pin() { * * ***************************************************************************************/ -void parse_direct_pin_name(char* src_string, int line, int* start_pin_index, int* end_pin_index, char* pb_type_name, char* port_name) { +void parse_direct_pin_name(const char* src_string, int line, int* start_pin_index, int* end_pin_index, char* pb_type_name, char* port_name) { /* Parses out the pb_type_name and port_name from the direct passed in. * * If the start_pin_index and end_pin_index is specified, parse them too. * * Return the values parsed by reference. */ char source_string[MAX_STRING_LEN + 1]; - char* find_format = nullptr; int ichar, match_count; if (vtr::split(src_string).size() > 1) { @@ -1716,7 +1715,7 @@ void parse_direct_pin_name(char* src_string, int line, int* start_pin_index, int } // parse out the pb_type and port name, possibly pin_indices - find_format = strstr(src_string, "["); + const char* find_format = strstr(src_string, "["); if (find_format == nullptr) { /* Format "pb_type_name.port_name" */ *start_pin_index = *end_pin_index = -1; @@ -1791,7 +1790,7 @@ static void mark_direct_of_pins(int start_pin_index, std::vector>& direct_type_from_blk_pin, int direct_type, int line, - char* src_string) { + const char* src_string) { /* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * * direct_type_from_blk_pin with direct_type from start_pin_index to * * end_pin_index. */ @@ -1837,7 +1836,7 @@ static void mark_direct_of_ports(int idirect, char* port_name, int end_pin_index, int start_pin_index, - char* src_string, + const char* src_string, int line, std::vector>& idirect_from_blk_pin, std::vector>& direct_type_from_blk_pin) { @@ -1890,8 +1889,7 @@ static void mark_direct_of_ports(int idirect, } // Finish going through all the blocks } -void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, - int num_directs, +void alloc_and_load_idirect_from_blk_pin(const std::vector& directs, std::vector>& idirect_from_blk_pin, std::vector>& direct_type_from_blk_pin) { /* Allocates and loads idirect_from_blk_pin and direct_type_from_blk_pin arrays. * @@ -1911,9 +1909,6 @@ void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, * Stores the pointers to the two 2D arrays in the addresses passed in. * * * * The two arrays are freed by the caller(s). */ - - int idirect, num_type_pins; - char to_pb_type_name[MAX_STRING_LEN + 1], to_port_name[MAX_STRING_LEN + 1], from_pb_type_name[MAX_STRING_LEN + 1], from_port_name[MAX_STRING_LEN + 1]; @@ -1930,7 +1925,7 @@ void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, } int itype = type.index; - num_type_pins = type.num_pins; + int num_type_pins = type.num_pins; idirect_from_blk_pin[itype].resize(num_type_pins, OPEN); direct_type_from_blk_pin[itype].resize(num_type_pins, OPEN); @@ -1938,13 +1933,13 @@ void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, /* Load the values */ // Go through directs and find pins with possible direct connections - for (idirect = 0; idirect < num_directs; idirect++) { + for (size_t idirect = 0; idirect < directs.size(); idirect++) { // Parse out the pb_type and port name, possibly pin_indices from from_pin - parse_direct_pin_name(directs[idirect].from_pin, directs[idirect].line, + parse_direct_pin_name(directs[idirect].from_pin.c_str(), directs[idirect].line, &from_end_pin_index, &from_start_pin_index, from_pb_type_name, from_port_name); // Parse out the pb_type and port name, possibly pin_indices from to_pin - parse_direct_pin_name(directs[idirect].to_pin, directs[idirect].line, + parse_direct_pin_name(directs[idirect].to_pin.c_str(), directs[idirect].line, &to_end_pin_index, &to_start_pin_index, to_pb_type_name, to_port_name); /* Now I have all the data that I need, I could go through all the block pins * @@ -1954,13 +1949,13 @@ void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, // Find blocks with the same name as from_pb_type_name and from_port_name mark_direct_of_ports(idirect, SOURCE, from_pb_type_name, from_port_name, - from_end_pin_index, from_start_pin_index, directs[idirect].from_pin, + from_end_pin_index, from_start_pin_index, directs[idirect].from_pin.c_str(), directs[idirect].line, idirect_from_blk_pin, direct_type_from_blk_pin); // Then, find blocks with the same name as to_pb_type_name and from_port_name mark_direct_of_ports(idirect, SINK, to_pb_type_name, to_port_name, - to_end_pin_index, to_start_pin_index, directs[idirect].to_pin, + to_end_pin_index, to_start_pin_index, directs[idirect].to_pin.c_str(), directs[idirect].line, idirect_from_blk_pin, direct_type_from_blk_pin); diff --git a/vpr/src/util/vpr_utils.h b/vpr/src/util/vpr_utils.h index 8b205a019ea..5d6ae9992e4 100644 --- a/vpr/src/util/vpr_utils.h +++ b/vpr/src/util/vpr_utils.h @@ -212,12 +212,11 @@ t_pin_range get_pb_pins(t_physical_tile_type_ptr physical_type, float compute_primitive_base_cost(const t_pb_graph_node* primitive); int num_ext_inputs_atom_block(AtomBlockId blk_id); -void alloc_and_load_idirect_from_blk_pin(t_direct_inf* directs, - int num_directs, +void alloc_and_load_idirect_from_blk_pin(const std::vector& directs, std::vector>& idirect_from_blk_pin, std::vector>& direct_type_from_blk_pin); -void parse_direct_pin_name(char* src_string, int line, int* start_pin_index, int* end_pin_index, char* pb_type_name, char* port_name); +void parse_direct_pin_name(const char* src_string, int line, int* start_pin_index, int* end_pin_index, char* pb_type_name, char* port_name); void free_pb_stats(t_pb* pb); void free_pb(t_pb* pb); diff --git a/vpr/test/test_connection_router.cpp b/vpr/test/test_connection_router.cpp index 1bc208bf3ba..595e9100db0 100644 --- a/vpr/test/test_connection_router.cpp +++ b/vpr/test/test_connection_router.cpp @@ -165,7 +165,6 @@ TEST_CASE("connection_router", "[vpr]") { &vpr_setup.RoutingArch, vpr_setup.Segments, arch.Directs, - arch.num_directs, router_opts.flat_routing); // Find a source and sink to route From f75afe01fc662fe367912fbd182f5c638e62a515 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Sat, 21 Sep 2024 16:19:03 -0400 Subject: [PATCH 004/162] add prints --- vpr/test/test_odd_even_routing.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vpr/test/test_odd_even_routing.cpp b/vpr/test/test_odd_even_routing.cpp index e642518076b..6b9435268df 100644 --- a/vpr/test/test_odd_even_routing.cpp +++ b/vpr/test/test_odd_even_routing.cpp @@ -5,6 +5,7 @@ #include "channel_dependency_graph.h" #include +#include namespace { @@ -232,6 +233,8 @@ TEST_CASE("test_route_flow", "[vpr_noc_odd_even_routing]") { NocTrafficFlows traffic_flow_storage; + std::cout << "Before for loop" << std::endl; + for (int i = 0; i < 100; i++) { auto src_blk_id = (ClusterBlockId)dist(rand_num_gen); @@ -240,20 +243,28 @@ TEST_CASE("test_route_flow", "[vpr_noc_odd_even_routing]") { dst_blk_id = (ClusterBlockId)dist(rand_num_gen); } while (src_blk_id == dst_blk_id); + std::cout << "before call i: " << i << std::endl; traffic_flow_storage.create_noc_traffic_flow("dummy_name_1", "dummy_name_2", src_blk_id, dst_blk_id, 1, 1, 1); + std::cout << "after call i: " << i << std::endl; } + + std::cout << "before finished "<< std::endl; traffic_flow_storage.finished_noc_traffic_flows_setup(); + std::cout << "after finished "<< std::endl; vtr::vector> traffic_flow_routes(traffic_flow_storage.get_number_of_traffic_flows()); for (const auto& [id, traffic_flow] : traffic_flow_storage.get_all_traffic_flows().pairs()) { + std::cout << (size_t)id << " " << (size_t)traffic_flow.source_router_cluster_id << " " << (size_t)traffic_flow.sink_router_cluster_id << std::endl; NocRouterId src_router_id = noc_model.get_router_at_grid_location(block_locs[traffic_flow.source_router_cluster_id].loc); NocRouterId dst_router_id = noc_model.get_router_at_grid_location(block_locs[traffic_flow.sink_router_cluster_id].loc); + std::cout <<"before route" << std::endl; REQUIRE_NOTHROW(routing_algorithm.route_flow(src_router_id, dst_router_id, id, traffic_flow_routes[id], noc_model)); + std::cout <<"after route" << std::endl; } ChannelDependencyGraph cdg(noc_model, traffic_flow_storage, traffic_flow_routes, block_locs); From 0a0554509ed5c5fb11b40d20ae99880653a9afe5 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Sat, 21 Sep 2024 17:05:56 -0400 Subject: [PATCH 005/162] move prints --- vpr/test/test_odd_even_routing.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vpr/test/test_odd_even_routing.cpp b/vpr/test/test_odd_even_routing.cpp index 6b9435268df..6396f2dedee 100644 --- a/vpr/test/test_odd_even_routing.cpp +++ b/vpr/test/test_odd_even_routing.cpp @@ -233,8 +233,6 @@ TEST_CASE("test_route_flow", "[vpr_noc_odd_even_routing]") { NocTrafficFlows traffic_flow_storage; - std::cout << "Before for loop" << std::endl; - for (int i = 0; i < 100; i++) { auto src_blk_id = (ClusterBlockId)dist(rand_num_gen); @@ -243,21 +241,23 @@ TEST_CASE("test_route_flow", "[vpr_noc_odd_even_routing]") { dst_blk_id = (ClusterBlockId)dist(rand_num_gen); } while (src_blk_id == dst_blk_id); - std::cout << "before call i: " << i << std::endl; traffic_flow_storage.create_noc_traffic_flow("dummy_name_1", "dummy_name_2", src_blk_id, dst_blk_id, 1, 1, 1); - std::cout << "after call i: " << i << std::endl; } - - std::cout << "before finished "<< std::endl; traffic_flow_storage.finished_noc_traffic_flows_setup(); std::cout << "after finished "<< std::endl; vtr::vector> traffic_flow_routes(traffic_flow_storage.get_number_of_traffic_flows()); + std::cout << "Size: " << traffic_flow_routes.size() << std::endl; + + std::cout << "getting pairs" << std::endl; + std::cout << "getting pairs " << traffic_flow_storage.get_all_traffic_flows().size() << std::endl; + const auto& all_pairs = traffic_flow_storage.get_all_traffic_flows().pairs(); + std::cout << "got pairs" << std::endl; - for (const auto& [id, traffic_flow] : traffic_flow_storage.get_all_traffic_flows().pairs()) { + for (const auto& [id, traffic_flow] : all_pairs) { - std::cout << (size_t)id << " " << (size_t)traffic_flow.source_router_cluster_id << " " << (size_t)traffic_flow.sink_router_cluster_id << std::endl; + std::cout << "loop: " << (size_t)id << " " << (size_t)traffic_flow.source_router_cluster_id << " " << (size_t)traffic_flow.sink_router_cluster_id << std::endl; NocRouterId src_router_id = noc_model.get_router_at_grid_location(block_locs[traffic_flow.source_router_cluster_id].loc); NocRouterId dst_router_id = noc_model.get_router_at_grid_location(block_locs[traffic_flow.sink_router_cluster_id].loc); From 4d2ef04e2ca1650b3318572a9068f4eb7ba01632 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 24 Sep 2024 16:38:31 -0400 Subject: [PATCH 006/162] created PlaceMacros class and moved some code --- vpr/src/place/place_macro.cpp | 336 +++++++++++++++++++++++++--------- vpr/src/place/place_macro.h | 80 ++++++-- vpr/src/util/vpr_utils.cpp | 213 --------------------- vpr/src/util/vpr_utils.h | 4 - 4 files changed, 314 insertions(+), 319 deletions(-) diff --git a/vpr/src/place/place_macro.cpp b/vpr/src/place/place_macro.cpp index aacb0559066..bef1e781bd7 100644 --- a/vpr/src/place/place_macro.cpp +++ b/vpr/src/place/place_macro.cpp @@ -7,7 +7,6 @@ #include #include "vtr_assert.h" -#include "vtr_memory.h" #include "vtr_util.h" #include "vpr_utils.h" #include "vpr_types.h" @@ -16,54 +15,51 @@ #include "globals.h" #include "echo_files.h" -/******************** File-scope variables declarations **********************/ - -/* f_idirect_from_blk_pin array allow us to quickly find pins that could be in a * - * direct connection. Values stored is the index of the possible direct connection * - * as specified in the arch file, OPEN (-1) is stored for pins that could not be * - * part of a direct chain connection. * - * [0...device_ctx.num_block_types-1][0...num_pins-1] */ -static std::vector> f_idirect_from_blk_pin; - -/* f_direct_type_from_blk_pin array stores the value SOURCE if the pin is the * - * from_pin, SINK if the pin is the to_pin in the direct connection as specified in * - * the arch file, OPEN (-1) is stored for pins that could not be part of a direct * - * chain connection. * - * [0...device_ctx.num_block_types-1][0...num_pins-1] */ -static std::vector> f_direct_type_from_blk_pin; - -/* f_imacro_from_blk_pin maps a blk_num to the corresponding macro index. * - * If the block is not part of a macro, the value OPEN (-1) is stored. * - * [0...cluster_ctx.clb_nlist.blocks().size()-1] */ -static vtr::vector_map f_imacro_from_iblk; - -/******************** Subroutine declarations ********************************/ - -static int find_all_the_macro(std::vector& pl_macro_member_blk_num_of_this_blk, - std::vector& pl_macro_idirect, - std::vector& pl_macro_num_members, - std::vector>& pl_macro_member_blk_num); - -static void alloc_and_load_imacro_from_iblk(const std::vector& macros); - -static void write_place_macros(std::string filename, const std::vector& macros); - static bool is_constant_clb_net(ClusterNetId clb_net); -static bool net_is_driven_by_direct(ClusterNetId clb_net); - static void validate_macros(const std::vector& macros); static bool try_combine_macros(std::vector>& pl_macro_member_blk_num, int matching_macro, int latest_macro); -/******************** Subroutine definitions *********************************/ -static int find_all_the_macro(std::vector& pl_macro_member_blk_num_of_this_blk, - std::vector& pl_macro_idirect, - std::vector& pl_macro_num_members, - std::vector>& pl_macro_member_blk_num) { +/* Go through all the ports in all the blocks to find the port that has the same * + * name as port_name and belongs to the block type that has the name pb_type_name. * + * Then, check that whether start_pin_index and end_pin_index are specified. If * + * they are, mark down the pins from start_pin_index to end_pin_index, inclusive. * + * Otherwise, mark down all the pins in that port. */ +static void mark_direct_of_ports(int idirect, + int direct_type, + char* pb_type_name, + char* port_name, + int end_pin_index, + int start_pin_index, + const char* src_string, + int line, + std::vector>& idirect_from_blk_pin, + std::vector>& direct_type_from_blk_pin); + +/* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * + * direct_type_from_blk_pin with direct_type from start_pin_index to * + * end_pin_index. */ +static void mark_direct_of_pins(int start_pin_index, + int end_pin_index, + int itype, + int isub_tile, + int iport, + std::vector>& idirect_from_blk_pin, + int idirect, + std::vector>& direct_type_from_blk_pin, + int direct_type, + int line, + const char* src_string); + + +int PlaceMacros::find_all_the_macro_(std::vector& pl_macro_member_blk_num_of_this_blk, + std::vector& pl_macro_idirect, + std::vector& pl_macro_num_members, + std::vector>& pl_macro_member_blk_num) { /* Compute required size: * * Go through all the pins with possible direct connections in * - * f_idirect_from_blk_pin. Count the number of heads (which is the same * + * idirect_from_blk_pin_. Count the number of heads (which is the same * * as the number macros) and also the length of each macro * * Head - blocks with to_pin OPEN and from_pin connected * * Tail - blocks with to_pin connected and from_pin OPEN */ @@ -89,8 +85,8 @@ static int find_all_the_macro(std::vector& pl_macro_member_blk_n int to_physical_pin = get_physical_pin(physical_tile, logical_block, to_iblk_pin); to_net_id = cluster_ctx.clb_nlist.block_net(blk_id, to_iblk_pin); - to_idirect = f_idirect_from_blk_pin[physical_tile->index][to_physical_pin]; - to_src_or_sink = f_direct_type_from_blk_pin[physical_tile->index][to_physical_pin]; + to_idirect = idirect_from_blk_pin_[physical_tile->index][to_physical_pin]; + to_src_or_sink = direct_type_from_blk_pin_[physical_tile->index][to_physical_pin]; // Identify potential macro head blocks (i.e. start of a macro) // @@ -104,13 +100,13 @@ static int find_all_the_macro(std::vector& pl_macro_member_blk_n if (to_src_or_sink == SINK && to_idirect != OPEN && (to_net_id == ClusterNetId::INVALID() || (is_constant_clb_net(to_net_id) - && !net_is_driven_by_direct(to_net_id)))) { + && !net_is_driven_by_direct_(to_net_id)))) { for (from_iblk_pin = 0; from_iblk_pin < num_blk_pins; from_iblk_pin++) { int from_physical_pin = get_physical_pin(physical_tile, logical_block, from_iblk_pin); from_net_id = cluster_ctx.clb_nlist.block_net(blk_id, from_iblk_pin); - from_idirect = f_idirect_from_blk_pin[physical_tile->index][from_physical_pin]; - from_src_or_sink = f_direct_type_from_blk_pin[physical_tile->index][from_physical_pin]; + from_idirect = idirect_from_blk_pin_[physical_tile->index][from_physical_pin]; + from_src_or_sink = direct_type_from_blk_pin_[physical_tile->index][from_physical_pin]; // Confirm whether this is a head macro // @@ -140,8 +136,8 @@ static int find_all_the_macro(std::vector& pl_macro_member_blk_n next_blk_id = cluster_ctx.clb_nlist.net_pin_block(curr_net_id, 1); // Assume that the from_iblk_pin index is the same for the next block - VTR_ASSERT(f_idirect_from_blk_pin[physical_tile->index][from_physical_pin] == from_idirect - && f_direct_type_from_blk_pin[physical_tile->index][from_physical_pin] == SOURCE); + VTR_ASSERT(idirect_from_blk_pin_[physical_tile->index][from_physical_pin] == from_idirect + && direct_type_from_blk_pin_[physical_tile->index][from_physical_pin] == SOURCE); next_net_id = cluster_ctx.clb_nlist.block_net(next_blk_id, from_iblk_pin); // Mark down this block as a member of the macro @@ -303,8 +299,8 @@ static bool try_combine_macros(std::vector>& pl_macr return true; } -std::vector alloc_and_load_placement_macros(const std::vector& directs) { - /* This function allocates and loads the macros placement macros * +std::vector PlaceMacros::alloc_and_load_placement_macros(const std::vector& directs) { + /* This function allocates and loads placement macros * * and returns the total number of macros in 2 steps. * * 1) Allocate temporary data structure for maximum possible * * size and loops through all the blocks storing the data * @@ -323,7 +319,6 @@ std::vector alloc_and_load_placement_macros(const std::vector alloc_and_load_placement_macros(const std::vector> pl_macro_member_blk_num(cluster_ctx.clb_nlist.blocks().size()); std::vector pl_macro_member_blk_num_of_this_blk(cluster_ctx.clb_nlist.blocks().size()); - /* Sets up the required variables. */ - alloc_and_load_idirect_from_blk_pin(directs, f_idirect_from_blk_pin, f_direct_type_from_blk_pin); + alloc_and_load_idirect_from_blk_pin_(directs); /* Compute required size: * * Go through all the pins with possible direct connections in * - * f_idirect_from_blk_pin. Count the number of heads (which is the same * + * idirect_from_blk_pin_. Count the number of heads (which is the same * * as the number macros) and also the length of each macro * * Head - blocks with to_pin OPEN and from_pin connected * * Tail - blocks with to_pin connected and from_pin OPEN */ - int num_macro = find_all_the_macro(pl_macro_member_blk_num_of_this_blk, - pl_macro_idirect, pl_macro_num_members, pl_macro_member_blk_num); + int num_macro = find_all_the_macro_(pl_macro_member_blk_num_of_this_blk, + pl_macro_idirect, pl_macro_num_members, pl_macro_member_blk_num); /* Allocate the memories for the macro. */ std::vector macros(num_macro); @@ -362,7 +356,7 @@ std::vector alloc_and_load_placement_macros(const std::vector alloc_and_load_placement_macros(const std::vector& macros) { - /* This mapping is needed for fast lookup's whether the block with index * +int PlaceMacros::get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros) { + /* This mapping is needed for fast lookups whether the block with index * * iblk belongs to a placement macro or not. * * * - * The array f_imacro_from_iblk is used for the mapping for speed reason * + * The array imacro_from_iblk_ is used for the mapping for speed reason * * [0...cluster_ctx.clb_nlist.blocks().size()-1] */ /* If the array is not allocated and loaded, allocate it. */ - if (f_imacro_from_iblk.empty()) { - alloc_and_load_imacro_from_iblk(macros); + if (imacro_from_iblk_.empty()) { + alloc_and_load_imacro_from_iblk_(macros); } int imacro; if (iblk) { /* Return the imacro for the block. */ - imacro = f_imacro_from_iblk[iblk]; + imacro = imacro_from_iblk_[iblk]; } else { imacro = OPEN; //No valid block, so no valid macro } @@ -394,40 +388,216 @@ int get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& mac return imacro; } -void set_imacro_for_iblk(int imacro, ClusterBlockId blk_id) { +void PlaceMacros::set_imacro_for_iblk(int imacro, ClusterBlockId blk_id) { auto& cluster_ctx = g_vpr_ctx.clustering(); - f_imacro_from_iblk.resize(cluster_ctx.clb_nlist.blocks().size()); - f_imacro_from_iblk.insert(blk_id, imacro); + imacro_from_iblk_.resize(cluster_ctx.clb_nlist.blocks().size()); + imacro_from_iblk_.insert(blk_id, imacro); +} + +void PlaceMacros::alloc_and_load_idirect_from_blk_pin_(const std::vector& directs) { + /* Allocates and loads idirect_from_blk_pin and direct_type_from_blk_pin arrays. * + * * + * For a bus (multiple bits) direct connection, all the pins in the bus are marked. * + * * + * idirect_from_blk_pin array allow us to quickly find pins that could be in a * + * direct connection. Values stored is the index of the possible direct connection * + * as specified in the arch file, OPEN (-1) is stored for pins that could not be * + * part of a direct chain connection. * + * * + * direct_type_from_blk_pin array stores the value SOURCE if the pin is the * + * from_pin, SINK if the pin is the to_pin in the direct connection as specified in * + * the arch file, OPEN (-1) is stored for pins that could not be part of a direct * + * chain connection. * + * * + * Stores the pointers to the two 2D arrays in the addresses passed in. * + * * + * The two arrays are freed by the caller(s). */ + constexpr size_t MAX_STRING_LEN = 512; + + char to_pb_type_name[MAX_STRING_LEN + 1], to_port_name[MAX_STRING_LEN + 1], + from_pb_type_name[MAX_STRING_LEN + 1], from_port_name[MAX_STRING_LEN + 1]; + + int to_start_pin_index = -1, to_end_pin_index = -1; + int from_start_pin_index = -1, from_end_pin_index = -1; + auto& device_ctx = g_vpr_ctx.device(); + + /* Allocate and initialize the values to OPEN (-1). */ + idirect_from_blk_pin_.resize(device_ctx.physical_tile_types.size()); + direct_type_from_blk_pin_.resize(device_ctx.physical_tile_types.size()); + for (const auto& type : device_ctx.physical_tile_types) { + if (is_empty_type(&type)) { + continue; + } + + int itype = type.index; + int num_type_pins = type.num_pins; + + idirect_from_blk_pin_[itype].resize(num_type_pins, OPEN); + direct_type_from_blk_pin_[itype].resize(num_type_pins, OPEN); + } + + /* Load the values */ + // Go through directs and find pins with possible direct connections + for (size_t idirect = 0; idirect < directs.size(); idirect++) { + // Parse out the pb_type and port name, possibly pin_indices from from_pin + parse_direct_pin_name(directs[idirect].from_pin.c_str(), directs[idirect].line, + &from_end_pin_index, &from_start_pin_index, from_pb_type_name, from_port_name); + + // Parse out the pb_type and port name, possibly pin_indices from to_pin + parse_direct_pin_name(directs[idirect].to_pin.c_str(), directs[idirect].line, + &to_end_pin_index, &to_start_pin_index, to_pb_type_name, to_port_name); + + /* Now I have all the data that I need, I could go through all the block pins * + * in all the blocks to find all the pins that could have possible direct * + * connections. Mark all down all those pins with the idirect the pins belong * + * to and whether it is a source or a sink of the direct connection. */ + + // Find blocks with the same name as from_pb_type_name and from_port_name + mark_direct_of_ports(idirect, SOURCE, from_pb_type_name, from_port_name, + from_end_pin_index, from_start_pin_index, directs[idirect].from_pin.c_str(), + directs[idirect].line, + idirect_from_blk_pin_, direct_type_from_blk_pin_); + + // Then, find blocks with the same name as to_pb_type_name and from_port_name + mark_direct_of_ports(idirect, SINK, to_pb_type_name, to_port_name, + to_end_pin_index, to_start_pin_index, directs[idirect].to_pin.c_str(), + directs[idirect].line, + idirect_from_blk_pin_, direct_type_from_blk_pin_); + + } // Finish going through all the directs +} + +static void mark_direct_of_ports(int idirect, + int direct_type, + char* pb_type_name, + char* port_name, + int end_pin_index, + int start_pin_index, + const char* src_string, + int line, + std::vector>& idirect_from_blk_pin, + std::vector>& direct_type_from_blk_pin) { + /* Go through all the ports in all the blocks to find the port that has the same * + * name as port_name and belongs to the block type that has the name pb_type_name. * + * Then, check that whether start_pin_index and end_pin_index are specified. If * + * they are, mark down the pins from start_pin_index to end_pin_index, inclusive. * + * Otherwise, mark down all the pins in that port. */ + + auto& device_ctx = g_vpr_ctx.device(); + + // Go through all the block types + for (int itype = 1; itype < (int)device_ctx.physical_tile_types.size(); itype++) { + auto& physical_tile = device_ctx.physical_tile_types[itype]; + // Find blocks with the same pb_type_name + if (strcmp(physical_tile.name, pb_type_name) == 0) { + int num_sub_tiles = physical_tile.sub_tiles.size(); + for (int isub_tile = 0; isub_tile < num_sub_tiles; isub_tile++) { + auto& ports = physical_tile.sub_tiles[isub_tile].ports; + int num_ports = ports.size(); + for (int iport = 0; iport < num_ports; iport++) { + // Find ports with the same port_name + if (strcmp(ports[iport].name, port_name) == 0) { + int num_port_pins = ports[iport].num_pins; + + // Check whether the end_pin_index is valid + if (end_pin_index > num_port_pins) { + VTR_LOG_ERROR( + "[LINE %d] Invalid pin - %s, the end_pin_index in " + "[end_pin_index:start_pin_index] should " + "be less than the num_port_pins %d.\n", + line, src_string, num_port_pins); + exit(1); + } + + // Check whether the pin indices are specified + if (start_pin_index >= 0 || end_pin_index >= 0) { + mark_direct_of_pins(start_pin_index, end_pin_index, itype, + isub_tile, iport, idirect_from_blk_pin, idirect, + direct_type_from_blk_pin, direct_type, line, src_string); + } else { + mark_direct_of_pins(0, num_port_pins - 1, itype, + isub_tile, iport, idirect_from_blk_pin, idirect, + direct_type_from_blk_pin, direct_type, line, src_string); + } + } // Do nothing if port_name does not match + } // Finish going through all the ports + } // Finish going through all the subtiles + } // Do nothing if pb_type_name does not match + } // Finish going through all the blocks +} + +static void mark_direct_of_pins(int start_pin_index, + int end_pin_index, + int itype, + int isub_tile, + int iport, + std::vector>& idirect_from_blk_pin, + int idirect, + std::vector>& direct_type_from_blk_pin, + int direct_type, + int line, + const char* src_string) { + /* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * + * direct_type_from_blk_pin with direct_type from start_pin_index to * + * end_pin_index. */ + + int iport_pin, iblk_pin; + auto& device_ctx = g_vpr_ctx.device(); + + // Mark pins with indices from start_pin_index to end_pin_index, inclusive + for (iport_pin = start_pin_index; iport_pin <= end_pin_index; iport_pin++) { + get_blk_pin_from_port_pin(itype, isub_tile, iport, iport_pin, &iblk_pin); + + //iterate through all segment connections and check if all Fc's are 0 + bool all_fcs_0 = true; + for (const auto& fc_spec : device_ctx.physical_tile_types[itype].fc_specs) { + for (int ipin : fc_spec.pins) { + if (iblk_pin == ipin && fc_spec.fc_value > 0) { + all_fcs_0 = false; + break; + } + } + if (!all_fcs_0) break; + } + + // Check the fc for the pin, direct chain link only if fc == 0 + if (all_fcs_0) { + idirect_from_blk_pin[itype][iblk_pin] = idirect; + + // Check whether the pins are marked, errors out if so + if (direct_type_from_blk_pin[itype][iblk_pin] != OPEN) { + VPR_FATAL_ERROR(VPR_ERROR_ARCH, + "[LINE %d] Invalid pin - %s, this pin is in more than one direct connection.\n", + line, src_string); + } else { + direct_type_from_blk_pin[itype][iblk_pin] = direct_type; + } + } + } // Finish marking all the pins } /* Allocates and loads imacro_from_iblk array. */ -static void alloc_and_load_imacro_from_iblk(const std::vector& macros) { +void PlaceMacros::alloc_and_load_imacro_from_iblk_(const std::vector& macros) { auto& cluster_ctx = g_vpr_ctx.clustering(); - f_imacro_from_iblk.resize(cluster_ctx.clb_nlist.blocks().size()); + imacro_from_iblk_.resize(cluster_ctx.clb_nlist.blocks().size()); /* Allocate and initialize the values to OPEN (-1). */ for (auto blk_id : cluster_ctx.clb_nlist.blocks()) { - f_imacro_from_iblk.insert(blk_id, OPEN); + imacro_from_iblk_.insert(blk_id, OPEN); } /* Load the values */ for (size_t imacro = 0; imacro < macros.size(); imacro++) { for (size_t imember = 0; imember < macros[imacro].members.size(); imember++) { ClusterBlockId blk_id = macros[imacro].members[imember].blk_index; - f_imacro_from_iblk.insert(blk_id, imacro); + imacro_from_iblk_.insert(blk_id, imacro); } } } -void free_placement_macros_structs() { - /* This function frees up all the static data structures used. */ - vtr::release_memory(f_idirect_from_blk_pin); - vtr::release_memory(f_direct_type_from_blk_pin); -} - -static void write_place_macros(std::string filename, const std::vector& macros) { +void PlaceMacros::write_place_macros_(std::string filename, const std::vector& macros) { FILE* f = vtr::fopen(filename.c_str(), "w"); auto& cluster_ctx = g_vpr_ctx.clustering(); @@ -463,15 +633,15 @@ static void write_place_macros(std::string filename, const std::vectorindex][physical_pin]; + auto direct = idirect_from_blk_pin_[physical_tile->index][physical_pin]; return direct != OPEN; } diff --git a/vpr/src/place/place_macro.h b/vpr/src/place/place_macro.h index a33771175ab..5419ca97dde 100644 --- a/vpr/src/place/place_macro.h +++ b/vpr/src/place/place_macro.h @@ -14,21 +14,6 @@ * treated as a unit and regular routing would not be used to connect the carry_in's and * carry_out's. Floorplanning constraints may also be an example of placement macros. * - * The function alloc_and_load_placement_macros allocates and loads the placement - * macros in the following steps: - * (1) First, go through all the block types and mark down the pins that could possibly - * be part of a placement macros. - * (2) Then, go through the netlist of all the pins marked in (1) to find out all the - * heads of the placement macros using criteria depending on the type of placement - * macros. For carry chains, the heads of the placement macros are blocks with - * carry_in's not connected to any nets (OPEN) while the carry_out's connected to the - * netlist with only 1 SINK. - * (3) Traverse from the heads to the tails of the placement macros and load the - * information in the t_pl_macro data structure. Similar to (2), tails are identified - * with criteria depending on the type of placement macros. For carry chains, the - * tails are blocks with carry_out's not connected to any nets (OPEN) while the - * carry_in's is connected to the netlist which has only 1 SINK. - * * The only placement macros supported at the moment are the carry chains with limited * functionality. * @@ -162,13 +147,70 @@ struct t_pl_macro { std::vector members; }; +class PlaceMacros { + public: + /** + * @brief Allocates and loads the placement macros. + * @details The following steps are taken in this methodL + * (1) First, go through all the block types and mark down the pins that could possibly + * be part of a placement macros. + * (2) Then, go through the netlist of all the pins marked in (1) to find out all the + * heads of the placement macros using criteria depending on the type of placement + * macros. For carry chains, the heads of the placement macros are blocks with + * carry_in's not connected to any nets (OPEN) while the carry_out's connected to the + * netlist with only 1 SINK. + * (3) Traverse from the heads to the tails of the placement macros and load the + * information in the t_pl_macro data structure. Similar to (2), tails are identified + * with criteria depending on the type of placement macros. For carry chains, the + * tails are blocks with carry_out's not connected to any nets (OPEN) while the + * carry_in's is connected to the netlist which has only 1 SINK. + * @param directs + * @return + */ + std::vector alloc_and_load_placement_macros(const std::vector& directs); + + int get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros); + + void set_imacro_for_iblk(int imacro, ClusterBlockId blk_id); + + private: + + /** + * @brief This array allow us to quickly find pins that could be in a direct connection. + * @details Values stored is the index of the possible direct connection as specified in the arch file, + * OPEN (-1) is stored for pins that could not be part of a direct chain connection. + * [0...device_ctx.num_block_types-1][0...num_pins-1] + */ + std::vector> idirect_from_blk_pin_; -std::vector alloc_and_load_placement_macros(const std::vector& directs); + /** + * @brief This array stores the value SOURCE if the pin is the from_pin, + * SINK if the pin is the to_pin in the direct connection as specified in the arch file, + * OPEN (-1) is stored for pins that could not be part of a direct chain connection. + * [0...device_ctx.num_block_types-1][0...num_pins-1] + */ + std::vector> direct_type_from_blk_pin_; -int get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros); + /** + * @brief Maps a blk_num to the corresponding macro index. + * @details If the block is not part of a macro, the value OPEN (-1) is stored. + * [0...cluster_ctx.clb_nlist.blocks().size()-1] + */ + vtr::vector_map imacro_from_iblk_; -void set_imacro_for_iblk(int imacro, ClusterBlockId iblk); + private: + int find_all_the_macro_(std::vector& pl_macro_member_blk_num_of_this_blk, + std::vector& pl_macro_idirect, + std::vector& pl_macro_num_members, + std::vector>& pl_macro_member_blk_num); -void free_placement_macros_structs(); + void alloc_and_load_imacro_from_iblk_(const std::vector& macros); + + void write_place_macros_(std::string filename, const std::vector& macros); + + bool net_is_driven_by_direct_(ClusterNetId clb_net); + + void alloc_and_load_idirect_from_blk_pin_(const std::vector& directs); +}; #endif diff --git a/vpr/src/util/vpr_utils.cpp b/vpr/src/util/vpr_utils.cpp index b6e4f057410..76d973014b5 100644 --- a/vpr/src/util/vpr_utils.cpp +++ b/vpr/src/util/vpr_utils.cpp @@ -52,37 +52,7 @@ const std::regex LOGIC_MODEL_REGEX("(.subckt\\s+)?.*(lut|names|lcell).*", std::r /* Allocates and loads blk_pin_from_port_pin array. */ static void alloc_and_load_blk_pin_from_port_pin(); -/* Go through all the ports in all the blocks to find the port that has the same * - * name as port_name and belongs to the block type that has the name pb_type_name. * - * Then, check that whether start_pin_index and end_pin_index are specified. If * - * they are, mark down the pins from start_pin_index to end_pin_index, inclusive. * - * Otherwise, mark down all the pins in that port. */ -static void mark_direct_of_ports(int idirect, - int direct_type, - char* pb_type_name, - char* port_name, - int end_pin_index, - int start_pin_index, - const char* src_string, - int line, - std::vector>& idirect_from_blk_pin, - std::vector>& direct_type_from_blk_pin); - static void get_blk_pin_from_port_pin(int blk_type_index, int sub_tile, int port, int port_pin, int* blk_pin); -/* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * - * direct_type_from_blk_pin with direct_type from start_pin_index to * - * end_pin_index. */ -static void mark_direct_of_pins(int start_pin_index, - int end_pin_index, - int itype, - int isub_tile, - int iport, - std::vector>& idirect_from_blk_pin, - int idirect, - std::vector>& direct_type_from_blk_pin, - int direct_type, - int line, - const char* src_string); static void load_pb_graph_pin_lookup_from_index_rec(t_pb_graph_pin** pb_graph_pin_lookup_from_index, t_pb_graph_node* pb_graph_node); @@ -1787,189 +1757,6 @@ void parse_direct_pin_name(const char* src_string, int line, int* start_pin_inde } } -static void mark_direct_of_pins(int start_pin_index, - int end_pin_index, - int itype, - int isub_tile, - int iport, - std::vector>& idirect_from_blk_pin, - int idirect, - std::vector>& direct_type_from_blk_pin, - int direct_type, - int line, - const char* src_string) { - /* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * - * direct_type_from_blk_pin with direct_type from start_pin_index to * - * end_pin_index. */ - - int iport_pin, iblk_pin; - auto& device_ctx = g_vpr_ctx.device(); - - // Mark pins with indices from start_pin_index to end_pin_index, inclusive - for (iport_pin = start_pin_index; iport_pin <= end_pin_index; iport_pin++) { - get_blk_pin_from_port_pin(itype, isub_tile, iport, iport_pin, &iblk_pin); - - //iterate through all segment connections and check if all Fc's are 0 - bool all_fcs_0 = true; - for (const auto& fc_spec : device_ctx.physical_tile_types[itype].fc_specs) { - for (int ipin : fc_spec.pins) { - if (iblk_pin == ipin && fc_spec.fc_value > 0) { - all_fcs_0 = false; - break; - } - } - if (!all_fcs_0) break; - } - - // Check the fc for the pin, direct chain link only if fc == 0 - if (all_fcs_0) { - idirect_from_blk_pin[itype][iblk_pin] = idirect; - - // Check whether the pins are marked, errors out if so - if (direct_type_from_blk_pin[itype][iblk_pin] != OPEN) { - VPR_FATAL_ERROR(VPR_ERROR_ARCH, - "[LINE %d] Invalid pin - %s, this pin is in more than one direct connection.\n", - line, src_string); - } else { - direct_type_from_blk_pin[itype][iblk_pin] = direct_type; - } - } - } // Finish marking all the pins -} - -static void mark_direct_of_ports(int idirect, - int direct_type, - char* pb_type_name, - char* port_name, - int end_pin_index, - int start_pin_index, - const char* src_string, - int line, - std::vector>& idirect_from_blk_pin, - std::vector>& direct_type_from_blk_pin) { - /* Go through all the ports in all the blocks to find the port that has the same * - * name as port_name and belongs to the block type that has the name pb_type_name. * - * Then, check that whether start_pin_index and end_pin_index are specified. If * - * they are, mark down the pins from start_pin_index to end_pin_index, inclusive. * - * Otherwise, mark down all the pins in that port. */ - - auto& device_ctx = g_vpr_ctx.device(); - - // Go through all the block types - for (int itype = 1; itype < (int)device_ctx.physical_tile_types.size(); itype++) { - auto& physical_tile = device_ctx.physical_tile_types[itype]; - // Find blocks with the same pb_type_name - if (strcmp(physical_tile.name, pb_type_name) == 0) { - int num_sub_tiles = physical_tile.sub_tiles.size(); - for (int isub_tile = 0; isub_tile < num_sub_tiles; isub_tile++) { - auto& ports = physical_tile.sub_tiles[isub_tile].ports; - int num_ports = ports.size(); - for (int iport = 0; iport < num_ports; iport++) { - // Find ports with the same port_name - if (strcmp(ports[iport].name, port_name) == 0) { - int num_port_pins = ports[iport].num_pins; - - // Check whether the end_pin_index is valid - if (end_pin_index > num_port_pins) { - VTR_LOG_ERROR( - "[LINE %d] Invalid pin - %s, the end_pin_index in " - "[end_pin_index:start_pin_index] should " - "be less than the num_port_pins %d.\n", - line, src_string, num_port_pins); - exit(1); - } - - // Check whether the pin indices are specified - if (start_pin_index >= 0 || end_pin_index >= 0) { - mark_direct_of_pins(start_pin_index, end_pin_index, itype, - isub_tile, iport, idirect_from_blk_pin, idirect, - direct_type_from_blk_pin, direct_type, line, src_string); - } else { - mark_direct_of_pins(0, num_port_pins - 1, itype, - isub_tile, iport, idirect_from_blk_pin, idirect, - direct_type_from_blk_pin, direct_type, line, src_string); - } - } // Do nothing if port_name does not match - } // Finish going through all the ports - } // Finish going through all the subtiles - } // Do nothing if pb_type_name does not match - } // Finish going through all the blocks -} - -void alloc_and_load_idirect_from_blk_pin(const std::vector& directs, - std::vector>& idirect_from_blk_pin, - std::vector>& direct_type_from_blk_pin) { - /* Allocates and loads idirect_from_blk_pin and direct_type_from_blk_pin arrays. * - * * - * For a bus (multiple bits) direct connection, all the pins in the bus are marked. * - * * - * idirect_from_blk_pin array allow us to quickly find pins that could be in a * - * direct connection. Values stored is the index of the possible direct connection * - * as specified in the arch file, OPEN (-1) is stored for pins that could not be * - * part of a direct chain connection. * - * * - * direct_type_from_blk_pin array stores the value SOURCE if the pin is the * - * from_pin, SINK if the pin is the to_pin in the direct connection as specified in * - * the arch file, OPEN (-1) is stored for pins that could not be part of a direct * - * chain connection. * - * * - * Stores the pointers to the two 2D arrays in the addresses passed in. * - * * - * The two arrays are freed by the caller(s). */ - char to_pb_type_name[MAX_STRING_LEN + 1], to_port_name[MAX_STRING_LEN + 1], - from_pb_type_name[MAX_STRING_LEN + 1], from_port_name[MAX_STRING_LEN + 1]; - - int to_start_pin_index = -1, to_end_pin_index = -1; - int from_start_pin_index = -1, from_end_pin_index = -1; - auto& device_ctx = g_vpr_ctx.device(); - - /* Allocate and initialize the values to OPEN (-1). */ - idirect_from_blk_pin.resize(device_ctx.physical_tile_types.size()); - direct_type_from_blk_pin.resize(device_ctx.physical_tile_types.size()); - for (const auto& type : device_ctx.physical_tile_types) { - if (is_empty_type(&type)) { - continue; - } - - int itype = type.index; - int num_type_pins = type.num_pins; - - idirect_from_blk_pin[itype].resize(num_type_pins, OPEN); - direct_type_from_blk_pin[itype].resize(num_type_pins, OPEN); - } - - /* Load the values */ - // Go through directs and find pins with possible direct connections - for (size_t idirect = 0; idirect < directs.size(); idirect++) { - // Parse out the pb_type and port name, possibly pin_indices from from_pin - parse_direct_pin_name(directs[idirect].from_pin.c_str(), directs[idirect].line, - &from_end_pin_index, &from_start_pin_index, from_pb_type_name, from_port_name); - - // Parse out the pb_type and port name, possibly pin_indices from to_pin - parse_direct_pin_name(directs[idirect].to_pin.c_str(), directs[idirect].line, - &to_end_pin_index, &to_start_pin_index, to_pb_type_name, to_port_name); - - /* Now I have all the data that I need, I could go through all the block pins * - * in all the blocks to find all the pins that could have possible direct * - * connections. Mark all down all those pins with the idirect the pins belong * - * to and whether it is a source or a sink of the direct connection. */ - - // Find blocks with the same name as from_pb_type_name and from_port_name - mark_direct_of_ports(idirect, SOURCE, from_pb_type_name, from_port_name, - from_end_pin_index, from_start_pin_index, directs[idirect].from_pin.c_str(), - directs[idirect].line, - idirect_from_blk_pin, direct_type_from_blk_pin); - - // Then, find blocks with the same name as to_pb_type_name and from_port_name - mark_direct_of_ports(idirect, SINK, to_pb_type_name, to_port_name, - to_end_pin_index, to_start_pin_index, directs[idirect].to_pin.c_str(), - directs[idirect].line, - idirect_from_blk_pin, direct_type_from_blk_pin); - - } // Finish going through all the directs - -} - /* * this function is only called by print_switch_usage() * at the point of this function call, every switch type / fanin combination diff --git a/vpr/src/util/vpr_utils.h b/vpr/src/util/vpr_utils.h index a7eff87b433..89dbc816e4c 100644 --- a/vpr/src/util/vpr_utils.h +++ b/vpr/src/util/vpr_utils.h @@ -216,10 +216,6 @@ t_pin_range get_pb_pins(t_physical_tile_type_ptr physical_type, float compute_primitive_base_cost(const t_pb_graph_node* primitive); int num_ext_inputs_atom_block(AtomBlockId blk_id); -void alloc_and_load_idirect_from_blk_pin(const std::vector& directs, - std::vector>& idirect_from_blk_pin, - std::vector>& direct_type_from_blk_pin); - void parse_direct_pin_name(const char* src_string, int line, int* start_pin_index, int* end_pin_index, char* pb_type_name, char* port_name); void free_pb_stats(t_pb* pb); From f6eff4e8baf5404b0eea4332ed1b7ec4d24b870a Mon Sep 17 00:00:00 2001 From: amin1377 Date: Thu, 26 Sep 2024 19:02:23 -0400 Subject: [PATCH 007/162] [libs][librrgraph] fix priniting node info --- libs/librrgraph/src/base/rr_graph_view.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/librrgraph/src/base/rr_graph_view.h b/libs/librrgraph/src/base/rr_graph_view.h index b96c7a615b4..d498c3ede1b 100644 --- a/libs/librrgraph/src/base/rr_graph_view.h +++ b/libs/librrgraph/src/base/rr_graph_view.h @@ -291,7 +291,7 @@ class RRGraphView { coordinate_string += " length:" + std::to_string(node_length(node)); //add the length of the segment //Figure out the starting and ending coordinate of the segment depending on the direction - arrow = "->"; //we will point the coordinates from start to finish, left to right + arrow = " ->"; //we will point the coordinates from start to finish, left to right if (node_direction(node) == Direction::DEC) { //signal travels along decreasing direction @@ -311,7 +311,7 @@ class RRGraphView { end_y = std::to_string(node_yhigh(node)) + ","; end_layer_str = std::to_string(node_layer_num) + ")"; //layer number if (node_direction(node) == Direction::BIDIR) { - arrow = "<->"; //indicate that signal can travel both direction + arrow = " <->"; //indicate that signal can travel both direction } } } From 816fb0a383f1f055c7bd2974ac68bab11fc0524f Mon Sep 17 00:00:00 2001 From: amin1377 Date: Thu, 26 Sep 2024 19:03:00 -0400 Subject: [PATCH 008/162] [libs][librrgraph] fix debugging mesg --- libs/librrgraph/src/utils/alloc_and_load_rr_indexed_data.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/librrgraph/src/utils/alloc_and_load_rr_indexed_data.cpp b/libs/librrgraph/src/utils/alloc_and_load_rr_indexed_data.cpp index 6b5634de641..90f7d0615ff 100644 --- a/libs/librrgraph/src/utils/alloc_and_load_rr_indexed_data.cpp +++ b/libs/librrgraph/src/utils/alloc_and_load_rr_indexed_data.cpp @@ -550,10 +550,10 @@ static void load_rr_indexed_data_T_values(const RRGraphView& rr_graph, calculate_average_switch(rr_graph, (size_t)rr_id, avg_switch_R, avg_switch_T, avg_switch_Cinternal, num_switches, buffered, fan_in_list); if (num_switches == 0) { - VTR_LOG_WARN("Node: %d with RR_type: %s at Location:%s, had no out-going switches\n", rr_id, + VTR_LOG_WARN("Node: %d with RR_type: %s at Location:%s, had no incoming switches\n", rr_id, rr_graph.node_type_string(rr_id), node_cords.c_str()); continue; - } + }all VTR_ASSERT(num_switches > 0); num_nodes_of_index[cost_index]++; From 64f426fec956c3db492d28ff83dcc1e2184c593c Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Mon, 30 Sep 2024 12:40:43 -0400 Subject: [PATCH 009/162] added PortPinToBlockPinConverter class --- vpr/src/place/place_macro.cpp | 43 +++++++++++++++++--- vpr/src/place/place_macro.h | 48 ++++++++++++++++++---- vpr/src/util/vpr_utils.cpp | 75 ----------------------------------- 3 files changed, 78 insertions(+), 88 deletions(-) diff --git a/vpr/src/place/place_macro.cpp b/vpr/src/place/place_macro.cpp index bef1e781bd7..2669a7508a2 100644 --- a/vpr/src/place/place_macro.cpp +++ b/vpr/src/place/place_macro.cpp @@ -541,15 +541,15 @@ static void mark_direct_of_pins(int start_pin_index, /* Mark the pin entry in idirect_from_blk_pin with idirect and the pin entry in * * direct_type_from_blk_pin with direct_type from start_pin_index to * * end_pin_index. */ - - int iport_pin, iblk_pin; auto& device_ctx = g_vpr_ctx.device(); + PortPinToBlockPinConverter port_pin_to_block_pin; + // Mark pins with indices from start_pin_index to end_pin_index, inclusive - for (iport_pin = start_pin_index; iport_pin <= end_pin_index; iport_pin++) { - get_blk_pin_from_port_pin(itype, isub_tile, iport, iport_pin, &iblk_pin); + for (int iport_pin = start_pin_index; iport_pin <= end_pin_index; iport_pin++) { + int iblk_pin = port_pin_to_block_pin.get_blk_pin_from_port_pin(itype, isub_tile, iport, iport_pin); - //iterate through all segment connections and check if all Fc's are 0 + // iterate through all segment connections and check if all Fc's are 0 bool all_fcs_0 = true; for (const auto& fc_spec : device_ctx.physical_tile_types[itype].fc_specs) { for (int ipin : fc_spec.pins) { @@ -703,3 +703,36 @@ static void validate_macros(const std::vector& macros) { } } } + +PortPinToBlockPinConverter::PortPinToBlockPinConverter() { + auto& device_ctx = g_vpr_ctx.device(); + auto& types = device_ctx.physical_tile_types; + + // Resize and initialize the values to OPEN (-1). + size_t num_types = types.size(); + blk_pin_from_port_pin_.resize(num_types); + + for (size_t itype = 1; itype < num_types; itype++) { + int blk_pin_count = 0; + auto& type = types[itype]; + size_t num_sub_tiles = type.sub_tiles.size(); + blk_pin_from_port_pin_[itype].resize(num_sub_tiles); + for (size_t isub_tile = 0; isub_tile < num_sub_tiles; isub_tile++) { + size_t num_ports = type.sub_tiles[isub_tile].ports.size(); + blk_pin_from_port_pin_[itype][isub_tile].resize(num_ports); + for (size_t iport = 0; iport < num_ports; iport++) { + int num_pins = type.sub_tiles[isub_tile].ports[iport].num_pins; + for (int ipin = 0; ipin < num_pins; ipin++) { + blk_pin_from_port_pin_[itype][isub_tile][iport].push_back(blk_pin_count); + blk_pin_count++; + } + } + } + } +} + +int PortPinToBlockPinConverter::get_blk_pin_from_port_pin(int blk_type_index, int sub_tile, int port, int port_pin) { + // Return the port and port_pin for the pin. + int blk_pin = blk_pin_from_port_pin_[blk_type_index][sub_tile][port][port_pin]; + return blk_pin; +} \ No newline at end of file diff --git a/vpr/src/place/place_macro.h b/vpr/src/place/place_macro.h index 5419ca97dde..652ad8015fd 100644 --- a/vpr/src/place/place_macro.h +++ b/vpr/src/place/place_macro.h @@ -129,21 +129,16 @@ * It is in the form of array of structs instead of * structs of arrays for cache efficiency. * Could have more data members for other macro type. - * blk_index: The cluster_ctx.blocks index of this block. - * x_offset: The x_offset from the first macro member to this member - * y_offset: The y_offset from the first macro member to this member - * z_offset: The z_offset from the first macro member to this member */ struct t_pl_macro_member { + ///@brief The cluster_ctx.blocks index of this block. ClusterBlockId blk_index; + ///@brief The offset from the first macro member to this member t_pl_offset offset; }; -/* num_blocks: The number of blocks this macro contains. - * members: An array of blocks in this macro [0:num_macro-1]. - * idirect: The direct index as specified in the arch file - */ struct t_pl_macro { + ///@brief An array of blocks in this macro [0:num_macro-1]. std::vector members; }; @@ -213,4 +208,41 @@ class PlaceMacros { void alloc_and_load_idirect_from_blk_pin_(const std::vector& directs); }; + +/** + * @class PortPinToBlockPinConverter + * @brief Maps the block pins indices for all block types to the corresponding port indices and port_pin indices. + * + * @details This is necessary since there are different netlist conventions - in the cluster level, + * ports and port pins are used while in the post-pack level, block pins are used. + */ +class PortPinToBlockPinConverter { + public: + /** + * @brief Allocates and loads blk_pin_from_port_pin_ array. + */ + PortPinToBlockPinConverter(); + + /** + * @brief Converts port and port pin indices of a specific block type to block pin index. + * + * @details The reason block type is used instead of blocks is to save memory. + * + * @param blk_type_index The block type index. + * @param sub_tile The subtile index within the specified block type. + * @param port The port number whose block pin number is desired. + * @param port_pin The port pin number in the specified port whose block pin number is desired. + * @return int The block pin index corresponding to the given port and port pin numbers. + */ + int get_blk_pin_from_port_pin(int blk_type_index, int sub_tile, int port, int port_pin); + + private: + /** + * @brief This array allows us to quickly find what block pin a port pin corresponds to. + * @details A 4D array that should be indexed as following: + * [0...device_ctx.physical_tile_types.size()-1][0..num_sub_tiles][0...num_ports-1][0...num_port_pins-1] + */ + std::vector>>> blk_pin_from_port_pin_; +}; + #endif diff --git a/vpr/src/util/vpr_utils.cpp b/vpr/src/util/vpr_utils.cpp index 76d973014b5..c66b6baddc3 100644 --- a/vpr/src/util/vpr_utils.cpp +++ b/vpr/src/util/vpr_utils.cpp @@ -31,29 +31,12 @@ static constexpr size_t MAX_STRING_LEN = 512; /******************** File-scope variables declarations **********************/ - -/* These three mappings are needed since there are two different netlist * - * conventions - in the cluster level, ports and port pins are used * - * while in the post-pack level, block pins are used. The reason block * - * type is used instead of blocks is to save memories. */ - -/* f_port_pin_to_block_pin array allows us to quickly find what block * - * pin a port pin corresponds to. * - * [0...device_ctx.physical_tile_types.size()-1][0..num_sub_tiles][0...num_ports-1][0...num_port_pins-1] */ -static std::vector>>> f_blk_pin_from_port_pin; - //Regular expressions used to determine register and logic primitives //TODO: Make this set-able from command-line? const std::regex REGISTER_MODEL_REGEX("(.subckt\\s+)?.*(latch|dff).*", std::regex::icase); const std::regex LOGIC_MODEL_REGEX("(.subckt\\s+)?.*(lut|names|lcell).*", std::regex::icase); /******************** Subroutine declarations ********************************/ - -/* Allocates and loads blk_pin_from_port_pin array. */ -static void alloc_and_load_blk_pin_from_port_pin(); - -static void get_blk_pin_from_port_pin(int blk_type_index, int sub_tile, int port, int port_pin, int* blk_pin); - static void load_pb_graph_pin_lookup_from_index_rec(t_pb_graph_pin** pb_graph_pin_lookup_from_index, t_pb_graph_node* pb_graph_node); static void load_pin_id_to_pb_mapping_rec(t_pb* cur_pb, t_pb** pin_id_to_pb_mapping); @@ -1604,64 +1587,6 @@ void free_pb_stats(t_pb* pb) { } } -/*************************************************************************************** - * Y.G.THIEN - * 29 AUG 2012 - * - * The following functions maps the block pins indices for all block types to the * - * corresponding port indices and port_pin indices. This is necessary since there are * - * different netlist conventions - in the cluster level, ports and port pins are used * - * while in the post-pack level, block pins are used. * - * * - ***************************************************************************************/ - -static void get_blk_pin_from_port_pin(int blk_type_index, int sub_tile, int port, int port_pin, int* blk_pin) { - /* This mapping is needed since there are two different netlist * - * conventions - in the cluster level, ports and port pins are used * - * while in the post-pack level, block pins are used. The reason block * - * type is used instead of blocks is to save memories. * - * * - * f_port_pin_to_block_pin array allows us to quickly find what block * - * pin a port pin corresponds to. * - * [0...device_ctx.logical_block_types.size()-1][0...num_ports-1][0...num_port_pins-1] */ - - /* If the array is not allocated and loaded, allocate it. */ - if (f_blk_pin_from_port_pin.empty()) { - alloc_and_load_blk_pin_from_port_pin(); - } - - /* Return the port and port_pin for the pin. */ - *blk_pin = f_blk_pin_from_port_pin[blk_type_index][sub_tile][port][port_pin]; -} - -static void alloc_and_load_blk_pin_from_port_pin() { - /* Allocates and loads blk_pin_from_port_pin array. */ - - auto& device_ctx = g_vpr_ctx.device(); - auto& types = device_ctx.physical_tile_types; - - /* Resize and initialize the values to OPEN (-1). */ - int num_types = types.size(); - f_blk_pin_from_port_pin.resize(num_types); - for (int itype = 1; itype < num_types; itype++) { - int blk_pin_count = 0; - auto& type = types[itype]; - int num_sub_tiles = type.sub_tiles.size(); - f_blk_pin_from_port_pin[itype].resize(num_sub_tiles); - for (int isub_tile = 0; isub_tile < num_sub_tiles; isub_tile++) { - int num_ports = type.sub_tiles[isub_tile].ports.size(); - f_blk_pin_from_port_pin[itype][isub_tile].resize(num_ports); - for (int iport = 0; iport < num_ports; iport++) { - int num_pins = type.sub_tiles[isub_tile].ports[iport].num_pins; - for (int ipin = 0; ipin < num_pins; ipin++) { - f_blk_pin_from_port_pin[itype][isub_tile][iport].push_back(blk_pin_count); - blk_pin_count++; - } - } - } - } -} - /*************************************************************************************** * Y.G.THIEN * 30 AUG 2012 From 97c97ff5549026eb13832b30cc452db14b6df8ff Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Mon, 30 Sep 2024 17:41:43 -0400 Subject: [PATCH 010/162] move placement macros from place_ctx to blk_loc_registry --- vpr/src/base/blk_loc_registry.cpp | 8 ++ vpr/src/base/blk_loc_registry.h | 8 +- vpr/src/base/vpr_api.cpp | 10 +- vpr/src/base/vpr_context.h | 3 - vpr/src/draw/draw_basic.cpp | 7 +- vpr/src/place/analytic_placer.cpp | 36 +++---- vpr/src/place/analytic_placer.h | 16 --- vpr/src/place/cut_spreader.cpp | 49 ++++----- vpr/src/place/initial_placement.cpp | 36 +++---- vpr/src/place/move_utils.cpp | 38 +++---- vpr/src/place/place.cpp | 19 ++-- vpr/src/place/place_constraints.cpp | 5 +- vpr/src/place/place_constraints.h | 2 +- vpr/src/place/place_macro.cpp | 154 +++++++++++++++------------- vpr/src/place/place_macro.h | 21 +++- vpr/src/place/place_util.cpp | 10 +- vpr/src/place/place_util.h | 5 +- 17 files changed, 218 insertions(+), 209 deletions(-) diff --git a/vpr/src/base/blk_loc_registry.cpp b/vpr/src/base/blk_loc_registry.cpp index 4c2f767b0f1..814385473ad 100644 --- a/vpr/src/base/blk_loc_registry.cpp +++ b/vpr/src/base/blk_loc_registry.cpp @@ -44,6 +44,14 @@ int BlkLocRegistry::net_pin_to_tile_pin_index(const ClusterNetId net_id, int net return this->tile_pin_index(pin_id); } +const PlaceMacros& BlkLocRegistry::place_macros() const { + return place_macros_; +} + +PlaceMacros& BlkLocRegistry::mutable_place_macros() { + return place_macros_; +} + void BlkLocRegistry::set_block_location(ClusterBlockId blk_id, const t_pl_loc& location) { auto& device_ctx = g_vpr_ctx.device(); auto& cluster_ctx = g_vpr_ctx.clustering(); diff --git a/vpr/src/base/blk_loc_registry.h b/vpr/src/base/blk_loc_registry.h index 542bad3651d..c9ea2589cb0 100644 --- a/vpr/src/base/blk_loc_registry.h +++ b/vpr/src/base/blk_loc_registry.h @@ -5,6 +5,7 @@ #include "vtr_vector_map.h" #include "vpr_types.h" #include "grid_block.h" +#include "place_macro.h" struct t_block_loc; struct t_pl_blocks_to_be_moved; @@ -16,7 +17,6 @@ struct t_pl_blocks_to_be_moved; * 2) grid_blocks stores which blocks (if any) are placed at a given location. * 3) physical_pins stores the mapping between the pins of a clustered block and * the pins of the physical tile where the clustered blocks is placed. - * */ class BlkLocRegistry { public: @@ -37,6 +37,8 @@ class BlkLocRegistry { ///@brief Clustered pin placement mapping with physical pin vtr::vector_map physical_pins_; + PlaceMacros place_macros_; + public: const vtr::vector_map& block_locs() const; vtr::vector_map& mutable_block_locs(); @@ -53,6 +55,10 @@ class BlkLocRegistry { ///@brief Returns the physical pin of the tile, related to the given ClusterNedId, and the net pin index. int net_pin_to_tile_pin_index(const ClusterNetId net_id, int net_pin_index) const; + const PlaceMacros& place_macros() const; + + PlaceMacros& mutable_place_macros(); + /** * @brief Performs error checking to see if location is legal for block type, * and sets the location and grid usage of the block if it is legal. diff --git a/vpr/src/base/vpr_api.cpp b/vpr/src/base/vpr_api.cpp index 3015658ccb3..bb3fc19d8f7 100644 --- a/vpr/src/base/vpr_api.cpp +++ b/vpr/src/base/vpr_api.cpp @@ -836,20 +836,16 @@ void vpr_load_placement(t_vpr_setup& vpr_setup, const t_arch& arch) { const auto& device_ctx = g_vpr_ctx.device(); auto& place_ctx = g_vpr_ctx.mutable_placement(); + auto& blk_loc_registry = place_ctx.mutable_blk_loc_registry(); const auto& filename_opts = vpr_setup.FileNameOpts; //Initialize placement data structures, which will be filled when loading placement - auto& block_locs = place_ctx.mutable_block_locs(); - GridBlock& grid_blocks = place_ctx.mutable_grid_blocks(); - init_placement_context(block_locs, grid_blocks); + init_placement_context(blk_loc_registry, arch.Directs); //Load an existing placement from a file place_ctx.placement_id = read_place(filename_opts.NetFile.c_str(), filename_opts.PlaceFile.c_str(), - place_ctx.mutable_blk_loc_registry(), + blk_loc_registry, filename_opts.verify_file_digests, device_ctx.grid); - - //Ensure placement macros are loaded so that they can be drawn after placement (e.g. during routing) - place_ctx.pl_macros = alloc_and_load_placement_macros(arch.Directs); } RouteStatus vpr_route_flow(const Netlist<>& net_list, diff --git a/vpr/src/base/vpr_context.h b/vpr/src/base/vpr_context.h index d684527f2cd..73e17ecec34 100644 --- a/vpr/src/base/vpr_context.h +++ b/vpr/src/base/vpr_context.h @@ -362,9 +362,6 @@ struct PlacementContext : public Context { */ void unlock_loc_vars() { VTR_ASSERT_SAFE(!loc_vars_are_accessible_); loc_vars_are_accessible_ = true; } - ///@brief The pl_macros array stores all the placement macros (usually carry chains). - std::vector pl_macros; - ///@brief Stores ClusterBlockId of all movable clustered blocks (blocks that are not locked down to a single location) std::vector movable_blocks; diff --git a/vpr/src/draw/draw_basic.cpp b/vpr/src/draw/draw_basic.cpp index 82ad456f70f..35fc8074d28 100644 --- a/vpr/src/draw/draw_basic.cpp +++ b/vpr/src/draw/draw_basic.cpp @@ -800,10 +800,11 @@ void draw_placement_macros(ezgl::renderer* g) { } t_draw_coords* draw_coords = get_draw_coords_vars(); - auto& place_ctx = g_vpr_ctx.placement(); - auto& block_locs = get_graphics_blk_loc_registry_ref().block_locs(); + const auto& blk_loc_registry = get_graphics_blk_loc_registry_ref(); + const auto& block_locs = blk_loc_registry.block_locs(); + const auto& place_macros = blk_loc_registry.place_macros(); - for (const t_pl_macro& pl_macro : place_ctx.pl_macros) { + for (const t_pl_macro& pl_macro : place_macros.macros()) { //TODO: for now we just draw the bounding box of the macro, which is incorrect for non-rectangular macros... int xlow = std::numeric_limits::max(); diff --git a/vpr/src/place/analytic_placer.cpp b/vpr/src/place/analytic_placer.cpp index a86f4f24e93..50f7c54f768 100644 --- a/vpr/src/place/analytic_placer.cpp +++ b/vpr/src/place/analytic_placer.cpp @@ -104,21 +104,6 @@ struct EquationSystem { } }; -// helper function to find the index of macro that contains blk -// returns index in placementCtx.pl_macros, -// returns NO_MACRO if blk not in any macros -int imacro(ClusterBlockId blk) { - int macro_index = get_imacro_from_iblk(blk, g_vpr_ctx.mutable_placement().pl_macros); - return macro_index; -} - -// helper fucntion to find the head (first block) of macro containing blk -// returns the ID of the head block -ClusterBlockId macro_head(ClusterBlockId blk) { - int macro_index = imacro(blk); - return g_vpr_ctx.mutable_placement().pl_macros[macro_index].members[0].blk_index; -} - // Stop optimizing once this many iterations of solve-legalize lead to negligible wirelength improvement constexpr int HEAP_STALLED_ITERATIONS_STOP = 15; @@ -303,6 +288,7 @@ void AnalyticPlacer::build_legal_locations() { void AnalyticPlacer::init() { const ClusteredNetlist& clb_nlist = g_vpr_ctx.clustering().clb_nlist; auto& init_block_locs = blk_loc_registry_ref_.block_locs(); + auto& place_macros = blk_loc_registry_ref_.place_macros(); for (auto blk_id : clb_nlist.blocks()) { blk_locs.insert(blk_id, BlockLocation{}); @@ -324,7 +310,7 @@ void AnalyticPlacer::init() { if (!init_block_locs[blk_id].is_fixed && has_connections(blk_id)) // not fixed and has connections // matrix equation is formulated based on connections, so requires at least one connection - if (imacro(blk_id) == NO_MACRO || macro_head(blk_id) == blk_id) { + if (place_macros.get_imacro_from_iblk(blk_id) == NO_MACRO || place_macros.macro_head(blk_id) == blk_id) { // not in macro or head of macro // for macro, only the head (base) block of the macro is a free variable, the location of other macro // blocks can be calculated using offset of the head. They are not free variables in the equation system @@ -385,6 +371,8 @@ int AnalyticPlacer::total_hpwl() { void AnalyticPlacer::setup_solve_blks(t_logical_block_type_ptr blkTypes) { const ClusteredNetlist& clb_nlist = g_vpr_ctx.clustering().clb_nlist; PlacementContext& place_ctx = g_vpr_ctx.mutable_placement(); + const auto& place_macros = blk_loc_registry_ref_.place_macros(); + int row = 0; solve_blks.clear(); @@ -400,9 +388,11 @@ void AnalyticPlacer::setup_solve_blks(t_logical_block_type_ptr blkTypes) { } } // update row_num of macro members - for (auto& macro : place_ctx.pl_macros) - for (auto& member : macro.members) - row_num[member.blk_index] = row_num[macro_head(member.blk_index)]; + for (auto& macro : blk_loc_registry_ref_.place_macros().macros()) { + for (auto& member : macro.members) { + row_num[member.blk_index] = row_num[place_macros.macro_head(member.blk_index)]; + } + } } /* @@ -411,7 +401,7 @@ void AnalyticPlacer::setup_solve_blks(t_logical_block_type_ptr blkTypes) { * when formulating the matrix equations), an update for members is necessary */ void AnalyticPlacer::update_macros() { - for (auto& macro : g_vpr_ctx.mutable_placement().pl_macros) { + for (auto& macro : blk_loc_registry_ref_.place_macros().macros()) { ClusterBlockId head_id = macro.members[0].blk_index; bool mac_can_be_placed = macro_can_be_placed(macro, blk_locs[head_id].loc, true, blk_loc_registry_ref_); @@ -474,7 +464,7 @@ void AnalyticPlacer::stamp_weight_on_matrix(EquationSystem& es, ClusterBlockId var, ClusterBlockId eqn, double weight) { - PlacementContext& place_ctx = g_vpr_ctx.mutable_placement(); + const auto& place_macros = blk_loc_registry_ref_.place_macros(); // Return the x or y position of a block auto blk_p = [&](ClusterBlockId blk_id) { return dir ? blk_locs[blk_id].loc.y : blk_locs[blk_id].loc.x; }; @@ -489,8 +479,8 @@ void AnalyticPlacer::stamp_weight_on_matrix(EquationSystem& es, } else { // var is not movable, stamp weight on rhs vector es.add_rhs(eqn_row, -v_pos * weight); } - if (imacro(var) != NO_MACRO) { // var is part of a macro, stamp on rhs vector - auto& members = place_ctx.pl_macros[imacro(var)].members; + if (place_macros.get_imacro_from_iblk(var) != NO_MACRO) { // var is part of a macro, stamp on rhs vector + auto& members = place_macros.macros()[place_macros.get_imacro_from_iblk(var)].members; for (auto& member : members) { // go through macro members to find the right member block if (member.blk_index == var) es.add_rhs(eqn_row, -(dir ? member.offset.y : member.offset.x) * weight); diff --git a/vpr/src/place/analytic_placer.h b/vpr/src/place/analytic_placer.h index a1f4ff8dcbe..b73b3486f57 100644 --- a/vpr/src/place/analytic_placer.h +++ b/vpr/src/place/analytic_placer.h @@ -99,22 +99,6 @@ extern int DONT_SOLVE; // sentinel for blks not part of a placement macro extern int NO_MACRO; -/* - * @brief helper function to find the index of macro that contains blk - * returns index in placementCtx.pl_macros, NO_MACRO if blk not in any macros - */ -int imacro(ClusterBlockId blk); - -/* - * @brief helper function to find the head block of the macro that contains blk - * placement macro head is the base of the macro, where the locations of the other macro members can be - * calculated using base.loc + member.offset. - * Only the placement of macro head is calculated directly from AP, the position of other macro members need - * to be calculated later using above formula. - * - * returns the ID of the head block - */ -ClusterBlockId macro_head(ClusterBlockId blk); class AnalyticPlacer { public: diff --git a/vpr/src/place/cut_spreader.cpp b/vpr/src/place/cut_spreader.cpp index fed8216795e..0bfd5356cea 100644 --- a/vpr/src/place/cut_spreader.cpp +++ b/vpr/src/place/cut_spreader.cpp @@ -112,6 +112,7 @@ void CutSpreader::cutSpread() { // setup CutSpreader data structures using information from AnalyticPlacer void CutSpreader::init() { const ClusteredNetlist& clb_nlist = g_vpr_ctx.clustering().clb_nlist; + const auto& place_macros = ap->blk_loc_registry_ref_.place_macros(); size_t max_x = g_vpr_ctx.device().grid.width(); size_t max_y = g_vpr_ctx.device().grid.height(); @@ -146,9 +147,9 @@ void CutSpreader::init() { auto loc = ap->blk_locs[blk].loc; occupancy[loc.x][loc.y]++; // compute extent of macro member - if (imacro(blk) != NO_MACRO) { // if blk is a macro member + if (place_macros.get_imacro_from_iblk(blk) != NO_MACRO) { // if blk is a macro member // only update macro heads' extent in blk_extents - set_macro_ext(macro_head(blk), loc.x, loc.y); + set_macro_ext(place_macros.macro_head(blk), loc.x, loc.y); } } } @@ -157,10 +158,10 @@ void CutSpreader::init() { ClusterBlockId blk = ClusterBlockId{(int)i}; if (clb_nlist.block_type(blk) == blk_type) { // Transfer macro extents to the actual macros structure; - if (imacro(blk) != NO_MACRO) { // if blk is a macro member + if (place_macros.get_imacro_from_iblk(blk) != NO_MACRO) { // if blk is a macro member // update macro_extent for all macro members in macros // for single blocks (not in macro), macros[x][y] = {x, y, x, y} - vtr::Rect& me = blk_extents[macro_head(blk)]; + vtr::Rect& me = blk_extents[place_macros.macro_head(blk)]; auto loc = ap->blk_locs[blk].loc; auto& lme = macro_extent[loc.x][loc.y]; lme.expand_bounding_box(me); @@ -406,7 +407,7 @@ void CutSpreader::expand_regions() { std::pair CutSpreader::cut_region(SpreaderRegion& r, bool dir) { const DeviceContext& device_ctx = g_vpr_ctx.device(); const ClusteredNetlist& clb_nlist = g_vpr_ctx.clustering().clb_nlist; - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = ap->blk_loc_registry_ref_.place_macros(); // TODO: CutSpreader is not compatible with 3D FPGA VTR_ASSERT(device_ctx.grid.get_num_layers() == 1); @@ -504,7 +505,7 @@ std::pair CutSpreader::cut_region(SpreaderRegion& r, bool dir) { // while left subarea is over-utilized, move logic blocks to the right subarea one at a time while (pivot > 0 && rl.overused(ap->ap_cfg.beta)) { auto& move_blk = cut_blks.at(pivot); - int size = (imacro(move_blk) != NO_MACRO) ? pl_macros[imacro(move_blk)].members.size() : 1; + int size = (place_macros.get_imacro_from_iblk(move_blk) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(move_blk)].members.size() : 1; rl.n_blks -= size; rr.n_blks += size; pivot--; @@ -512,7 +513,7 @@ std::pair CutSpreader::cut_region(SpreaderRegion& r, bool dir) { // while right subarea is over-utilized, move logic blocks to the left subarea one at a time while (pivot < int(cut_blks.size()) - 1 && rr.overused(ap->ap_cfg.beta)) { auto& move_blk = cut_blks.at(pivot + 1); - int size = (imacro(move_blk) != NO_MACRO) ? pl_macros[imacro(move_blk)].members.size() : 1; + int size = (place_macros.get_imacro_from_iblk(move_blk) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(move_blk)].members.size() : 1; rl.n_blks += size; rr.n_blks -= size; pivot++; @@ -618,7 +619,7 @@ int CutSpreader::initial_source_cut(SpreaderRegion& r, bool dir, int& clearance_l, int& clearance_r) { - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = ap->blk_loc_registry_ref_.place_macros(); // pivot is the midpoint of cut_blks in terms of total block size (counting macro members) // this ensures the initial partitions have similar number of blocks @@ -626,7 +627,7 @@ int CutSpreader::initial_source_cut(SpreaderRegion& r, int pivot = 0; // midpoint in terms of index of cut_blks for (auto& blk : cut_blks) { // if blk is part of macro (only macro heads in cut_blks, no macro members), add that macro's size - pivot_blks += (imacro(blk) != NO_MACRO) ? pl_macros[imacro(blk)].members.size() : 1; + pivot_blks += (place_macros.get_imacro_from_iblk(blk) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(blk)].members.size() : 1; if (pivot_blks >= r.n_blks / 2) break; pivot++; @@ -671,16 +672,16 @@ int CutSpreader::initial_target_cut(SpreaderRegion& r, int& right_blks_n, int& left_tiles_n, int& right_tiles_n) { - const auto& pl_macros = g_vpr_ctx.mutable_placement().pl_macros; + const auto& place_macros = ap->blk_loc_registry_ref_.place_macros(); // To achieve smallest difference in utilization, first move all tiles to right partition left_blks_n = 0, right_blks_n = 0; left_tiles_n = 0, right_tiles_n = r.n_tiles; // count number of blks in each partition, from initial source cut for (int i = 0; i <= init_source_cut; i++) - left_blks_n += (imacro(cut_blks.at(i)) != NO_MACRO) ? pl_macros[imacro(cut_blks.at(i))].members.size() : 1; + left_blks_n += (place_macros.get_imacro_from_iblk(cut_blks.at(i)) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(cut_blks.at(i))].members.size() : 1; for (int i = init_source_cut + 1; i < int(cut_blks.size()); i++) - right_blks_n += (imacro(cut_blks.at(i)) != NO_MACRO) ? pl_macros[imacro(cut_blks.at(i))].members.size() : 1; + right_blks_n += (place_macros.get_imacro_from_iblk(cut_blks.at(i)) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(cut_blks.at(i))].members.size() : 1; int best_tgt_cut = -1; double best_deltaU = std::numeric_limits::max(); @@ -807,13 +808,13 @@ void CutSpreader::linear_spread_subarea(std::vector& cut_blks, void CutSpreader::strict_legalize() { auto& clb_nlist = g_vpr_ctx.clustering().clb_nlist; const auto& block_locs = ap->blk_loc_registry_ref_.block_locs(); - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = ap->blk_loc_registry_ref_.place_macros(); int max_x = g_vpr_ctx.device().grid.width(); int max_y = g_vpr_ctx.device().grid.height(); // clear the location of all blocks in place_ctx for (auto blk : clb_nlist.blocks()) { - if (!block_locs[blk].is_fixed && (ap->row_num[blk] != DONT_SOLVE || (imacro(blk) != NO_MACRO && ap->row_num[macro_head(blk)] != DONT_SOLVE))) { + if (!block_locs[blk].is_fixed && (ap->row_num[blk] != DONT_SOLVE || (place_macros.get_imacro_from_iblk(blk) != NO_MACRO && ap->row_num[place_macros.macro_head(blk)] != DONT_SOLVE))) { unbind_tile(block_locs[blk].loc); } } @@ -824,10 +825,11 @@ void CutSpreader::strict_legalize() { // This prioritizes the placement of longest macros over single blocks std::priority_queue> remaining; for (ClusterBlockId blk : ap->solve_blks) { - if (imacro(blk) != NO_MACRO) // blk is head block of a macro (only head blks are solved) - remaining.emplace(pl_macros[imacro(blk)].members.size(), blk); - else + if (place_macros.get_imacro_from_iblk(blk) != NO_MACRO) { // blk is head block of a macro (only head blks are solved) + remaining.emplace(place_macros.macros()[place_macros.get_imacro_from_iblk(blk)].members.size(), blk); + } else { remaining.emplace(1, blk); + } } /* @@ -939,7 +941,7 @@ void CutSpreader::strict_legalize() { int explore_limit = 2 * radius; // if blk is not a macro member - if (imacro(blk) == NO_MACRO) { + if (place_macros.get_imacro_from_iblk(blk) == NO_MACRO) { placed = try_place_blk(blk, nx, ny, @@ -1033,6 +1035,7 @@ bool CutSpreader::try_place_blk(ClusterBlockId blk, std::priority_queue>& remaining) { const auto& grid_blocks = ap->blk_loc_registry_ref_.grid_blocks(); const ClusteredNetlist& clb_nlist = g_vpr_ctx.clustering().clb_nlist; + const auto& place_macros = ap->blk_loc_registry_ref_.place_macros(); // iteration at current radius has exceeded exploration limit, and a candidate sub_tile (best_subtile) is found // then blk is placed in best_subtile @@ -1060,7 +1063,7 @@ bool CutSpreader::try_place_blk(ClusterBlockId blk, * OR * 2) a 0.05% chance of acceptance. */ - if (bound_blk && imacro(bound_blk) != NO_MACRO) + if (bound_blk && place_macros.get_imacro_from_iblk(bound_blk) != NO_MACRO) // do not sub_tiles when the block placed on it is part of a macro, as they have higher priority continue; if (!exceeds_explore_limit) { // if still in exploration phase, find best_subtile with smallest best_inp_len @@ -1109,7 +1112,7 @@ bool CutSpreader::try_place_macro(ClusterBlockId blk, int nx, int ny, std::priority_queue>& remaining) { - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = ap->blk_loc_registry_ref_.place_macros(); const auto& grid_blocks = ap->blk_loc_registry_ref_.grid_blocks(); const ClusteredNetlist& clb_nlist = g_vpr_ctx.clustering().clb_nlist; @@ -1135,15 +1138,15 @@ bool CutSpreader::try_place_macro(ClusterBlockId blk, // if the target location has a logic block, ensure it's not part of a macro // because a macro placed before the current one has higher priority (longer chain) ClusterBlockId bound = grid_blocks.block_at_location(target); - if (bound && imacro(bound) != NO_MACRO) { + if (bound && place_macros.get_imacro_from_iblk(bound) != NO_MACRO) { placement_impossible = true; break; } // place macro block into target vector along with its target location targets.emplace_back(visit_blk, target); - if (macro_head(visit_blk) == visit_blk) { // if visit_blk is the head block of the macro + if (place_macros.macro_head(visit_blk) == visit_blk) { // if visit_blk is the head block of the macro // push all macro members to visit queue along with their calculated positions - const std::vector& members = pl_macros[imacro(blk)].members; + const std::vector& members = place_macros.macros()[place_macros.get_imacro_from_iblk(blk)].members; for (auto member = members.begin() + 1; member != members.end(); ++member) { t_pl_loc mloc = target + member->offset; // calculate member_loc using (head blk location + offset) visit.emplace(member->blk_index, mloc); diff --git a/vpr/src/place/initial_placement.cpp b/vpr/src/place/initial_placement.cpp index 0cf233e07d9..4e0e13ded1c 100644 --- a/vpr/src/place/initial_placement.cpp +++ b/vpr/src/place/initial_placement.cpp @@ -84,7 +84,7 @@ static bool place_macro(int macros_max_num_tries, * Used for relative placement, so that the blocks that are more difficult to place can be placed first during initial placement. * A higher score indicates that the block is more difficult to place. */ -static vtr::vector assign_block_scores(); +static vtr::vector assign_block_scores(const PlaceMacros& place_macros); /** * @brief Tries to find y coordinate for macro head location based on macro direction @@ -959,12 +959,9 @@ static bool place_macro(int macros_max_num_tries, return macro_placed; } -static vtr::vector assign_block_scores() { - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& place_ctx = g_vpr_ctx.placement(); - auto& floorplan_ctx = g_vpr_ctx.floorplanning(); - - auto& pl_macros = place_ctx.pl_macros; +static vtr::vector assign_block_scores(const PlaceMacros& place_macros) { + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& floorplan_ctx = g_vpr_ctx.floorplanning();; t_block_score score; @@ -994,7 +991,7 @@ static vtr::vector assign_block_scores() { } //go through placement macros and store size of macro for each block - for (const auto& pl_macro : pl_macros) { + for (const auto& pl_macro : place_macros.macros()) { int size = pl_macro.members.size(); for (const auto& pl_macro_member : pl_macro.members) { block_scores[pl_macro_member.blk_index].macro_size = size; @@ -1010,10 +1007,12 @@ static void place_all_blocks(const t_placer_opts& placer_opts, enum e_pad_loc_type pad_loc_type, const char* constraints_file, BlkLocRegistry& blk_loc_registry) { - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& place_ctx = g_vpr_ctx.placement(); - auto& device_ctx = g_vpr_ctx.device(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& device_ctx = g_vpr_ctx.device(); + const auto& place_macros = blk_loc_registry.place_macros(); + auto blocks = cluster_ctx.clb_nlist.blocks(); + int number_of_unplaced_blks_in_curr_itr; //keep tracks of which block types can not be placed in each iteration @@ -1082,7 +1081,7 @@ static void place_all_blocks(const t_placer_opts& placer_opts, //add current block to list to ensure it will be placed sooner in the next iteration in initial placement number_of_unplaced_blks_in_curr_itr++; block_scores[blk_id].failed_to_place_in_prev_attempts++; - int imacro = get_imacro_from_iblk(blk_id, place_ctx.pl_macros); + int imacro = place_macros.get_imacro_from_iblk(blk_id); if (imacro != -1) { //the block belongs to macro that contain a chain, we need to turn on dense placement in next iteration for that type of block unplaced_blk_type_in_curr_itr.insert(blk_id_type->index); } @@ -1171,8 +1170,8 @@ bool place_one_block(const ClusterBlockId blk_id, std::vector* blk_types_empty_locs_in_grid, vtr::vector* block_scores, BlkLocRegistry& blk_loc_registry) { - const std::vector& pl_macros = g_vpr_ctx.placement().pl_macros; const auto& block_locs = blk_loc_registry.block_locs(); + const auto& place_macros = blk_loc_registry.place_macros(); //Check if block has already been placed if (is_block_placed(blk_id, block_locs)) { @@ -1182,11 +1181,11 @@ bool place_one_block(const ClusterBlockId blk_id, bool placed_macro = false; //Lookup to see if the block is part of a macro - int imacro = get_imacro_from_iblk(blk_id, pl_macros); + int imacro = place_macros.get_imacro_from_iblk(blk_id); if (imacro != -1) { //If the block belongs to a macro, pass that macro to the placement routines VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\tBelongs to a macro %d\n", imacro); - const t_pl_macro& pl_macro = pl_macros[imacro]; + const t_pl_macro& pl_macro = place_macros.macros()[imacro]; placed_macro = place_macro(MAX_NUM_TRIES_TO_PLACE_MACROS_RANDOMLY, pl_macro, pad_loc_type, blk_types_empty_locs_in_grid, *block_scores, blk_loc_registry); } else { //If it does not belong to a macro, create a macro with the one block and then pass to the placement routines @@ -1232,6 +1231,7 @@ void initial_placement(const t_placer_opts& placer_opts, BlkLocRegistry& blk_loc_registry) { vtr::ScopedStartFinishTimer timer("Initial Placement"); auto& block_locs = blk_loc_registry.mutable_block_locs(); + const auto& place_macros = blk_loc_registry.place_macros(); /* Initialize the grid blocks to empty. * Initialize all the blocks to unplaced. @@ -1241,7 +1241,7 @@ void initial_placement(const t_placer_opts& placer_opts, /* Go through cluster blocks to calculate the tightest placement * floorplan constraint for each constrained block */ - propagate_place_constraints(); + propagate_place_constraints(place_macros); /*Mark the blocks that have already been locked to one spot via floorplan constraints * as fixed, so they do not get moved during initial placement or later during the simulated annealing stage of placement*/ @@ -1265,11 +1265,11 @@ void initial_placement(const t_placer_opts& placer_opts, if (noc_opts.noc) { // NoC routers are placed before other blocks initial_noc_placement(noc_opts, placer_opts, blk_loc_registry); - propagate_place_constraints(); + propagate_place_constraints(place_macros); } //Assign scores to blocks and placement macros according to how difficult they are to place - vtr::vector block_scores = assign_block_scores(); + vtr::vector block_scores = assign_block_scores(place_macros); //Place all blocks place_all_blocks(placer_opts, block_scores, placer_opts.pad_loc_type, constraints_file, blk_loc_registry); diff --git a/vpr/src/place/move_utils.cpp b/vpr/src/place/move_utils.cpp index 343263b4169..836511fb52f 100644 --- a/vpr/src/place/move_utils.cpp +++ b/vpr/src/place/move_utils.cpp @@ -85,13 +85,13 @@ e_block_move_result find_affected_blocks(t_pl_blocks_to_be_moved& blocks_affecte const auto& block_locs = blk_loc_registry.block_locs(); const GridBlock& grid_blocks = blk_loc_registry.grid_blocks(); - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = blk_loc_registry.place_macros(); e_block_move_result outcome = e_block_move_result::VALID; t_pl_loc from = block_locs[b_from].loc; - int imacro_from = get_imacro_from_iblk(b_from, pl_macros); + int imacro_from = place_macros.get_imacro_from_iblk(b_from); if (imacro_from != -1) { // b_from is part of a macro, I need to swap the whole macro @@ -101,11 +101,11 @@ e_block_move_result find_affected_blocks(t_pl_blocks_to_be_moved& blocks_affecte int imember_from = 0; outcome = record_macro_swaps(blocks_affected, imacro_from, imember_from, swap_offset, blk_loc_registry); - VTR_ASSERT_SAFE(outcome != e_block_move_result::VALID || imember_from == int(pl_macros[imacro_from].members.size())); + VTR_ASSERT_SAFE(outcome != e_block_move_result::VALID || imember_from == int(place_macros.macros()[imacro_from].members.size())); } else { ClusterBlockId b_to = grid_blocks.block_at_location(to); - int imacro_to = get_imacro_from_iblk(b_to, pl_macros); + int imacro_to = place_macros.get_imacro_from_iblk(b_to); if (imacro_to != -1) { //To block is a macro but from is a single block. @@ -180,16 +180,16 @@ e_block_move_result record_macro_swaps(t_pl_blocks_to_be_moved& blocks_affected, int& imember_from, t_pl_offset swap_offset, const BlkLocRegistry& blk_loc_registry) { - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = blk_loc_registry.place_macros(); const auto& block_locs = blk_loc_registry.block_locs(); const GridBlock& grid_blocks = blk_loc_registry.grid_blocks(); e_block_move_result outcome = e_block_move_result::VALID; - for (; imember_from < int(pl_macros[imacro_from].members.size()) && outcome == e_block_move_result::VALID; imember_from++) { + for (; imember_from < int(place_macros.macros()[imacro_from].members.size()) && outcome == e_block_move_result::VALID; imember_from++) { // Gets the new from and to info for every block in the macro // cannot use the old from and to info - ClusterBlockId curr_b_from = pl_macros[imacro_from].members[imember_from].blk_index; + ClusterBlockId curr_b_from = place_macros.macros()[imacro_from].members[imember_from].blk_index; t_pl_loc curr_from = block_locs[curr_b_from].loc; @@ -207,14 +207,14 @@ e_block_move_result record_macro_swaps(t_pl_blocks_to_be_moved& blocks_affected, outcome = e_block_move_result::ABORT; } else { ClusterBlockId b_to = grid_blocks.block_at_location(curr_to); - int imacro_to = get_imacro_from_iblk(b_to, pl_macros); + int imacro_to = place_macros.get_imacro_from_iblk(b_to); if (imacro_to != -1) { //To block is a macro if (imacro_from == imacro_to) { outcome = record_macro_self_swaps(blocks_affected, imacro_from, swap_offset, blk_loc_registry); - imember_from = pl_macros[imacro_from].members.size(); + imember_from = place_macros.macros()[imacro_from].members.size(); break; //record_macro_self_swaps() handles this case completely, so we don't need to continue the loop } else { outcome = record_macro_macro_swaps(blocks_affected, imacro_from, imember_from, imacro_to, b_to, swap_offset, blk_loc_registry); @@ -249,7 +249,7 @@ e_block_move_result record_macro_macro_swaps(t_pl_blocks_to_be_moved& blocks_aff //The position in the from macro ('imacro_from') is specified by 'imember_from', and the relevant //macro fro the to block is 'imacro_to'. - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& pl_macros = blk_loc_registry.place_macros().macros(); const auto& block_locs = blk_loc_registry.block_locs(); //At the moment, we only support blk_to being the first element of the 'to' macro. @@ -345,11 +345,11 @@ e_block_move_result record_macro_move(t_pl_blocks_to_be_moved& blocks_affected, const int imacro, t_pl_offset swap_offset, const BlkLocRegistry& blk_loc_registry) { - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = blk_loc_registry.place_macros(); const auto& block_locs = blk_loc_registry.block_locs(); const GridBlock& grid_blocks = blk_loc_registry.grid_blocks(); - for (const t_pl_macro_member& member : pl_macros[imacro].members) { + for (const t_pl_macro_member& member : place_macros.macros()[imacro].members) { t_pl_loc from = block_locs[member.blk_index].loc; t_pl_loc to = from + swap_offset; @@ -363,7 +363,7 @@ e_block_move_result record_macro_move(t_pl_blocks_to_be_moved& blocks_affected, blocks_affected.record_block_move(member.blk_index, to, blk_loc_registry); - int imacro_to = get_imacro_from_iblk(blk_to, pl_macros); + int imacro_to = place_macros.get_imacro_from_iblk(blk_to); if (blk_to && imacro_to != imacro) { //Block displaced only if exists and not part of current macro displaced_blocks.push_back(blk_to); } @@ -378,14 +378,14 @@ e_block_move_result identify_macro_self_swap_affected_macros(std::vector& m const int imacro, t_pl_offset swap_offset, const BlkLocRegistry& blk_loc_registry) { - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = blk_loc_registry.place_macros(); const auto& block_locs = blk_loc_registry.block_locs(); const GridBlock& grid_blocks = blk_loc_registry.grid_blocks(); e_block_move_result outcome = e_block_move_result::VALID; - for (size_t imember = 0; imember < pl_macros[imacro].members.size() && outcome == e_block_move_result::VALID; ++imember) { - ClusterBlockId blk = pl_macros[imacro].members[imember].blk_index; + for (size_t imember = 0; imember < place_macros.macros()[imacro].members.size() && outcome == e_block_move_result::VALID; ++imember) { + ClusterBlockId blk = place_macros.macros()[imacro].members[imember].blk_index; t_pl_loc from = block_locs[blk].loc; t_pl_loc to = from + swap_offset; @@ -397,7 +397,7 @@ e_block_move_result identify_macro_self_swap_affected_macros(std::vector& m ClusterBlockId blk_to = grid_blocks.block_at_location(to); - int imacro_to = get_imacro_from_iblk(blk_to, pl_macros); + int imacro_to = place_macros.get_imacro_from_iblk(blk_to); if (imacro_to != -1) { auto itr = std::find(macros.begin(), macros.end(), imacro_to); @@ -414,7 +414,7 @@ e_block_move_result record_macro_self_swaps(t_pl_blocks_to_be_moved& blocks_affe const int imacro, t_pl_offset swap_offset, const BlkLocRegistry& blk_loc_registry) { - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& place_macros = blk_loc_registry.place_macros(); //Reset any partial move blocks_affected.clear_move_blocks(); @@ -442,7 +442,7 @@ e_block_move_result record_macro_self_swaps(t_pl_blocks_to_be_moved& blocks_affe } auto is_non_macro_block = [&](ClusterBlockId blk) { - int imacro_blk = get_imacro_from_iblk(blk, pl_macros); + int imacro_blk = place_macros.get_imacro_from_iblk(blk); if (std::find(affected_macros.begin(), affected_macros.end(), imacro_blk) != affected_macros.end()) { return false; diff --git a/vpr/src/place/place.cpp b/vpr/src/place/place.cpp index d3edbd6e912..286ed01759b 100644 --- a/vpr/src/place/place.cpp +++ b/vpr/src/place/place.cpp @@ -353,8 +353,7 @@ void try_place(const Netlist<>& net_list, * width should be taken to when calculating costs. This allows a * * greater bias for anisotropic architectures. */ - /* - * Currently, the functions that require is_flat as their parameter and are called during placement should + /* Currently, the functions that require is_flat as their parameter and are called during placement should * receive is_flat as false. For example, if the RR graph of router lookahead is built here, it should be as * if is_flat is false, even if is_flat is set to true from the command line. */ @@ -606,13 +605,13 @@ void try_place(const Netlist<>& net_list, } size_t num_macro_members = 0; - for (auto& macro : g_vpr_ctx.placement().pl_macros) { + for (auto& macro : blk_loc_registry.place_macros().macros()) { num_macro_members += macro.members.size(); } VTR_LOG( "Placement contains %zu placement macros involving %zu blocks (average macro size %f)\n", - g_vpr_ctx.placement().pl_macros.size(), num_macro_members, - float(num_macro_members) / g_vpr_ctx.placement().pl_macros.size()); + blk_loc_registry.place_macros().macros().size(), num_macro_members, + float(num_macro_members) / blk_loc_registry.place_macros().macros().size()); VTR_LOG("\n"); sprintf(msg, @@ -1818,9 +1817,7 @@ static NetCostHandler alloc_and_load_placement_structs(const t_placer_opts& plac const int num_layers = device_ctx.grid.get_num_layers(); - auto& block_locs = placer_state.mutable_block_locs(); - auto& grid_blocks = placer_state.mutable_grid_blocks(); - init_placement_context(block_locs, grid_blocks); + init_placement_context(placer_state.mutable_blk_loc_registry(), directs); int max_pins_per_clb = 0; for (const t_physical_tile_type& type : device_ctx.physical_tile_types) { @@ -1874,8 +1871,6 @@ static NetCostHandler alloc_and_load_placement_structs(const t_placer_opts& plac elem = OPEN; } - place_ctx.pl_macros = alloc_and_load_placement_macros(directs); - if (noc_opts.noc) { allocate_and_load_noc_placement_structs(); } @@ -1889,8 +1884,6 @@ static NetCostHandler alloc_and_load_placement_structs(const t_placer_opts& plac /* Frees the major structures needed by the placer (and not needed * * elsewhere). */ static void free_placement_structs(const t_noc_opts& noc_opts) { - free_placement_macros_structs(); - auto& place_ctx = g_vpr_ctx.mutable_placement(); vtr::release_memory(place_ctx.compressed_block_grids); @@ -2058,7 +2051,7 @@ static int check_block_placement_consistency(const BlkLocRegistry& blk_loc_regis } int check_macro_placement_consistency(const BlkLocRegistry& blk_loc_registry) { - const auto& pl_macros = g_vpr_ctx.placement().pl_macros; + const auto& pl_macros = blk_loc_registry.place_macros().macros(); const auto& block_locs = blk_loc_registry.block_locs(); const auto& grid_blocks = blk_loc_registry.grid_blocks(); diff --git a/vpr/src/place/place_constraints.cpp b/vpr/src/place/place_constraints.cpp index 94af4721026..f53efc0f4ef 100644 --- a/vpr/src/place/place_constraints.cpp +++ b/vpr/src/place/place_constraints.cpp @@ -157,11 +157,10 @@ void print_macro_constraint_error(const t_pl_macro& pl_macro) { VPR_ERROR(VPR_ERROR_PLACE, " \n Check that the above-mentioned placement macro blocks have compatible floorplan constraints.\n"); } -void propagate_place_constraints() { - auto& place_ctx = g_vpr_ctx.placement(); +void propagate_place_constraints(const PlaceMacros& place_macros) { auto& floorplanning_ctx = g_vpr_ctx.mutable_floorplanning(); - for (const t_pl_macro& pl_macro : place_ctx.pl_macros) { + for (const t_pl_macro& pl_macro : place_macros.macros()) { if (is_macro_constrained(pl_macro)) { /* Get the PartitionRegion for the head of the macro * based on the constraints of all blocks contained in the macro diff --git a/vpr/src/place/place_constraints.h b/vpr/src/place/place_constraints.h index 02157e907a1..78497dd20f8 100644 --- a/vpr/src/place/place_constraints.h +++ b/vpr/src/place/place_constraints.h @@ -93,7 +93,7 @@ PartitionRegion update_macro_member_pr(const PartitionRegion& head_pr, * initial placement to ease floorplan legality checking while placing macros during * initial placement. */ -void propagate_place_constraints(); +void propagate_place_constraints(const PlaceMacros& place_macros); void print_macro_constraint_error(const t_pl_macro& pl_macro); diff --git a/vpr/src/place/place_macro.cpp b/vpr/src/place/place_macro.cpp index 2669a7508a2..43f591feb43 100644 --- a/vpr/src/place/place_macro.cpp +++ b/vpr/src/place/place_macro.cpp @@ -52,6 +52,85 @@ static void mark_direct_of_pins(int start_pin_index, int line, const char* src_string); +const std::vector& PlaceMacros::macros() const { + return pl_macros_; +} + +void PlaceMacros::alloc_and_load_placement_macros(const std::vector& directs) { + /* Allocates allocates and loads placement macros and returns + * the total number of macros in 2 steps. + * 1) Allocate temporary data structure for maximum possible + * size and loops through all the blocks storing the data + * relevant to the carry chains. At the same time, also count + * the amount of memory required for the actual variables. + * 2) Allocate the actual variables with the exact amount of + * memory. Then loads the data from the temporary data + * structures before freeing them. + * + * For pl_macro_member_blk_num, allocate for the first dimension + * only at first. Allocate for the second dimension when I know + * the size. Otherwise, the array is going to be of size + * cluster_ctx.clb_nlist.blocks().size()^2 (There are big + * benckmarks VPR that have cluster_ctx.clb_nlist.blocks().size() + * in the 100k's range). + * + * The placement macro array is freed by the caller(s). + */ + auto& cluster_ctx = g_vpr_ctx.clustering(); + + // Allocate maximum memory for temporary variables. + std::vector pl_macro_idirect(cluster_ctx.clb_nlist.blocks().size()); + std::vector pl_macro_num_members(cluster_ctx.clb_nlist.blocks().size()); + std::vector> pl_macro_member_blk_num(cluster_ctx.clb_nlist.blocks().size()); + std::vector pl_macro_member_blk_num_of_this_blk(cluster_ctx.clb_nlist.blocks().size()); + + alloc_and_load_idirect_from_blk_pin_(directs); + + /* Compute required size: + * Go through all the pins with possible direct connections in + * idirect_from_blk_pin_. Count the number of heads (which is the same + * as the number macros) and also the length of each macro + * Head - blocks with to_pin OPEN and from_pin connected + * Tail - blocks with to_pin connected and from_pin OPEN + */ + int num_macro = find_all_the_macro_(pl_macro_member_blk_num_of_this_blk, + pl_macro_idirect, pl_macro_num_members, pl_macro_member_blk_num); + + // Allocate the memories for the macro. + pl_macros_.resize(num_macro); + + /* Allocate the memories for the chain members. + * Load the values from the temporary data structures. + */ + for (int imacro = 0; imacro < num_macro; imacro++) { + pl_macros_[imacro].members = std::vector(pl_macro_num_members[imacro]); + + // Load the values for each member of the macro + for (size_t imember = 0; imember < pl_macros_[imacro].members.size(); imember++) { + pl_macros_[imacro].members[imember].offset.x = imember * directs[pl_macro_idirect[imacro]].x_offset; + pl_macros_[imacro].members[imember].offset.y = imember * directs[pl_macro_idirect[imacro]].y_offset; + pl_macros_[imacro].members[imember].offset.sub_tile = directs[pl_macro_idirect[imacro]].sub_tile_offset; + pl_macros_[imacro].members[imember].blk_index = pl_macro_member_blk_num[imacro][imember]; + } + } + + if (isEchoFileEnabled(E_ECHO_PLACE_MACROS)) { + write_place_macros_(getEchoFileName(E_ECHO_PLACE_MACROS), pl_macros_); + } + + validate_macros(pl_macros_); + + alloc_and_load_imacro_from_iblk_(pl_macros_); +} + +ClusterBlockId PlaceMacros::macro_head(ClusterBlockId blk) const { + int macro_index = get_imacro_from_iblk(blk); + if (macro_index == OPEN) { + return ClusterBlockId::INVALID(); + } else { + return pl_macros_[macro_index].members[0].blk_index; + } +} int PlaceMacros::find_all_the_macro_(std::vector& pl_macro_member_blk_num_of_this_blk, std::vector& pl_macro_idirect, @@ -299,84 +378,13 @@ static bool try_combine_macros(std::vector>& pl_macr return true; } -std::vector PlaceMacros::alloc_and_load_placement_macros(const std::vector& directs) { - /* This function allocates and loads placement macros * - * and returns the total number of macros in 2 steps. * - * 1) Allocate temporary data structure for maximum possible * - * size and loops through all the blocks storing the data * - * relevant to the carry chains. At the same time, also count * - * the amount of memory required for the actual variables. * - * 2) Allocate the actual variables with the exact amount of * - * memory. Then loads the data from the temporary data * - * structures before freeing them. * - * * - * For pl_macro_member_blk_num, allocate for the first dimension * - * only at first. Allocate for the second dimension when I know * - * the size. Otherwise, the array is going to be of size * - * cluster_ctx.clb_nlist.blocks().size()^2 (There are big * - * benckmarks VPR that have cluster_ctx.clb_nlist.blocks().size() * - * in the 100k's range). * - * * - * The placement macro array is freed by the caller(s). */ - - auto& cluster_ctx = g_vpr_ctx.clustering(); - - /* Allocate maximum memory for temporary variables. */ - std::vector pl_macro_idirect(cluster_ctx.clb_nlist.blocks().size()); - std::vector pl_macro_num_members(cluster_ctx.clb_nlist.blocks().size()); - std::vector> pl_macro_member_blk_num(cluster_ctx.clb_nlist.blocks().size()); - std::vector pl_macro_member_blk_num_of_this_blk(cluster_ctx.clb_nlist.blocks().size()); - - alloc_and_load_idirect_from_blk_pin_(directs); - - /* Compute required size: * - * Go through all the pins with possible direct connections in * - * idirect_from_blk_pin_. Count the number of heads (which is the same * - * as the number macros) and also the length of each macro * - * Head - blocks with to_pin OPEN and from_pin connected * - * Tail - blocks with to_pin connected and from_pin OPEN */ - int num_macro = find_all_the_macro_(pl_macro_member_blk_num_of_this_blk, - pl_macro_idirect, pl_macro_num_members, pl_macro_member_blk_num); - - /* Allocate the memories for the macro. */ - std::vector macros(num_macro); - - /* Allocate the memories for the chain members. * - * Load the values from the temporary data structures. */ - for (int imacro = 0; imacro < num_macro; imacro++) { - macros[imacro].members = std::vector(pl_macro_num_members[imacro]); - - /* Load the values for each member of the macro */ - for (size_t imember = 0; imember < macros[imacro].members.size(); imember++) { - macros[imacro].members[imember].offset.x = imember * directs[pl_macro_idirect[imacro]].x_offset; - macros[imacro].members[imember].offset.y = imember * directs[pl_macro_idirect[imacro]].y_offset; - macros[imacro].members[imember].offset.sub_tile = directs[pl_macro_idirect[imacro]].sub_tile_offset; - macros[imacro].members[imember].blk_index = pl_macro_member_blk_num[imacro][imember]; - } - } - - if (isEchoFileEnabled(E_ECHO_PLACE_MACROS)) { - write_place_macros_(getEchoFileName(E_ECHO_PLACE_MACROS), macros); - } - - validate_macros(macros); - - return macros; -} - - -int PlaceMacros::get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros) { +int PlaceMacros::get_imacro_from_iblk(ClusterBlockId iblk) const{ /* This mapping is needed for fast lookups whether the block with index * * iblk belongs to a placement macro or not. * * * * The array imacro_from_iblk_ is used for the mapping for speed reason * * [0...cluster_ctx.clb_nlist.blocks().size()-1] */ - /* If the array is not allocated and loaded, allocate it. */ - if (imacro_from_iblk_.empty()) { - alloc_and_load_imacro_from_iblk_(macros); - } - int imacro; if (iblk) { /* Return the imacro for the block. */ @@ -671,6 +679,8 @@ bool PlaceMacros::net_is_driven_by_direct_(ClusterNetId clb_net) { return direct != OPEN; } + + static void validate_macros(const std::vector& macros) { //Perform sanity checks on macros auto& cluster_ctx = g_vpr_ctx.clustering(); diff --git a/vpr/src/place/place_macro.h b/vpr/src/place/place_macro.h index 652ad8015fd..d275a486050 100644 --- a/vpr/src/place/place_macro.h +++ b/vpr/src/place/place_macro.h @@ -144,6 +144,8 @@ struct t_pl_macro { class PlaceMacros { public: + PlaceMacros() = default; + /** * @brief Allocates and loads the placement macros. * @details The following steps are taken in this methodL @@ -160,14 +162,24 @@ class PlaceMacros { * tails are blocks with carry_out's not connected to any nets (OPEN) while the * carry_in's is connected to the netlist which has only 1 SINK. * @param directs - * @return */ - std::vector alloc_and_load_placement_macros(const std::vector& directs); + void alloc_and_load_placement_macros(const std::vector& directs); - int get_imacro_from_iblk(ClusterBlockId iblk, const std::vector& macros); + int get_imacro_from_iblk(ClusterBlockId iblk) const; void set_imacro_for_iblk(int imacro, ClusterBlockId blk_id); + /** + * @brief Finds the head block of the macro that contains a given clustered block + * @details Placement macro head is the base of the macro, where the locations of the other macro members can be + * calculated using base.loc + member.offset. + * + * @return The Id of a clustered block that is the head of a macro that the given clustered block is part of. + */ + ClusterBlockId macro_head(ClusterBlockId blk) const; + + const std::vector& macros() const; + private: /** @@ -193,6 +205,9 @@ class PlaceMacros { */ vtr::vector_map imacro_from_iblk_; + ///@brief Stores all the placement macros (usually carry chains). + std::vector pl_macros_; + private: int find_all_the_macro_(std::vector& pl_macro_member_blk_num_of_this_blk, std::vector& pl_macro_idirect, diff --git a/vpr/src/place/place_util.cpp b/vpr/src/place/place_util.cpp index 3541ef01bf1..bc123ea0e31 100644 --- a/vpr/src/place/place_util.cpp +++ b/vpr/src/place/place_util.cpp @@ -17,16 +17,22 @@ */ static GridBlock init_grid_blocks(); -void init_placement_context(vtr::vector_map& block_locs, - GridBlock& grid_blocks) { +void init_placement_context(BlkLocRegistry& blk_loc_registry, + const std::vector& directs) { auto& cluster_ctx = g_vpr_ctx.clustering(); + auto& block_locs = blk_loc_registry.mutable_block_locs(); + auto& grid_blocks = blk_loc_registry.mutable_grid_blocks(); + auto& place_macros = blk_loc_registry.mutable_place_macros(); + /* Initialize the lookup of CLB block positions */ block_locs.clear(); block_locs.resize(cluster_ctx.clb_nlist.blocks().size()); /* Initialize the reverse lookup of CLB block positions */ grid_blocks = init_grid_blocks(); + + place_macros.alloc_and_load_placement_macros(directs); } static GridBlock init_grid_blocks() { diff --git a/vpr/src/place/place_util.h b/vpr/src/place/place_util.h index 934d2072251..4df609fc226 100644 --- a/vpr/src/place/place_util.h +++ b/vpr/src/place/place_util.h @@ -305,11 +305,12 @@ class t_placer_statistics { * * Forward direction - block to grid: place_ctx.block_locs. * Reverse direction - grid to block: place_ctx.grid_blocks. + * Allocates and load placement macros. * * Initialize both of them to empty states. */ -void init_placement_context(vtr::vector_map& block_locs, - GridBlock& grid_blocks); +void init_placement_context(BlkLocRegistry& blk_loc_registry, + const std::vector& directs); /** * @brief Get the initial limit for inner loop block move attempt limit. From 47cf976c002bc8ce8a84b53e8fe9953dd2ca076e Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Mon, 30 Sep 2024 17:48:39 -0400 Subject: [PATCH 011/162] add operator[] to PlaceMacros --- vpr/src/place/analytic_placer.cpp | 2 +- vpr/src/place/cut_spreader.cpp | 14 +++++++------- vpr/src/place/initial_placement.cpp | 2 +- vpr/src/place/move_utils.cpp | 14 +++++++------- vpr/src/place/place_macro.cpp | 8 ++++++++ vpr/src/place/place_macro.h | 3 +++ 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/vpr/src/place/analytic_placer.cpp b/vpr/src/place/analytic_placer.cpp index 50f7c54f768..95b43033322 100644 --- a/vpr/src/place/analytic_placer.cpp +++ b/vpr/src/place/analytic_placer.cpp @@ -480,7 +480,7 @@ void AnalyticPlacer::stamp_weight_on_matrix(EquationSystem& es, es.add_rhs(eqn_row, -v_pos * weight); } if (place_macros.get_imacro_from_iblk(var) != NO_MACRO) { // var is part of a macro, stamp on rhs vector - auto& members = place_macros.macros()[place_macros.get_imacro_from_iblk(var)].members; + auto& members = place_macros[place_macros.get_imacro_from_iblk(var)].members; for (auto& member : members) { // go through macro members to find the right member block if (member.blk_index == var) es.add_rhs(eqn_row, -(dir ? member.offset.y : member.offset.x) * weight); diff --git a/vpr/src/place/cut_spreader.cpp b/vpr/src/place/cut_spreader.cpp index 0bfd5356cea..c9c969b1abd 100644 --- a/vpr/src/place/cut_spreader.cpp +++ b/vpr/src/place/cut_spreader.cpp @@ -505,7 +505,7 @@ std::pair CutSpreader::cut_region(SpreaderRegion& r, bool dir) { // while left subarea is over-utilized, move logic blocks to the right subarea one at a time while (pivot > 0 && rl.overused(ap->ap_cfg.beta)) { auto& move_blk = cut_blks.at(pivot); - int size = (place_macros.get_imacro_from_iblk(move_blk) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(move_blk)].members.size() : 1; + int size = (place_macros.get_imacro_from_iblk(move_blk) != NO_MACRO) ? place_macros[place_macros.get_imacro_from_iblk(move_blk)].members.size() : 1; rl.n_blks -= size; rr.n_blks += size; pivot--; @@ -513,7 +513,7 @@ std::pair CutSpreader::cut_region(SpreaderRegion& r, bool dir) { // while right subarea is over-utilized, move logic blocks to the left subarea one at a time while (pivot < int(cut_blks.size()) - 1 && rr.overused(ap->ap_cfg.beta)) { auto& move_blk = cut_blks.at(pivot + 1); - int size = (place_macros.get_imacro_from_iblk(move_blk) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(move_blk)].members.size() : 1; + int size = (place_macros.get_imacro_from_iblk(move_blk) != NO_MACRO) ? place_macros[place_macros.get_imacro_from_iblk(move_blk)].members.size() : 1; rl.n_blks += size; rr.n_blks -= size; pivot++; @@ -627,7 +627,7 @@ int CutSpreader::initial_source_cut(SpreaderRegion& r, int pivot = 0; // midpoint in terms of index of cut_blks for (auto& blk : cut_blks) { // if blk is part of macro (only macro heads in cut_blks, no macro members), add that macro's size - pivot_blks += (place_macros.get_imacro_from_iblk(blk) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(blk)].members.size() : 1; + pivot_blks += (place_macros.get_imacro_from_iblk(blk) != NO_MACRO) ? place_macros[place_macros.get_imacro_from_iblk(blk)].members.size() : 1; if (pivot_blks >= r.n_blks / 2) break; pivot++; @@ -679,9 +679,9 @@ int CutSpreader::initial_target_cut(SpreaderRegion& r, left_tiles_n = 0, right_tiles_n = r.n_tiles; // count number of blks in each partition, from initial source cut for (int i = 0; i <= init_source_cut; i++) - left_blks_n += (place_macros.get_imacro_from_iblk(cut_blks.at(i)) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(cut_blks.at(i))].members.size() : 1; + left_blks_n += (place_macros.get_imacro_from_iblk(cut_blks.at(i)) != NO_MACRO) ? place_macros[place_macros.get_imacro_from_iblk(cut_blks.at(i))].members.size() : 1; for (int i = init_source_cut + 1; i < int(cut_blks.size()); i++) - right_blks_n += (place_macros.get_imacro_from_iblk(cut_blks.at(i)) != NO_MACRO) ? place_macros.macros()[place_macros.get_imacro_from_iblk(cut_blks.at(i))].members.size() : 1; + right_blks_n += (place_macros.get_imacro_from_iblk(cut_blks.at(i)) != NO_MACRO) ? place_macros[place_macros.get_imacro_from_iblk(cut_blks.at(i))].members.size() : 1; int best_tgt_cut = -1; double best_deltaU = std::numeric_limits::max(); @@ -826,7 +826,7 @@ void CutSpreader::strict_legalize() { std::priority_queue> remaining; for (ClusterBlockId blk : ap->solve_blks) { if (place_macros.get_imacro_from_iblk(blk) != NO_MACRO) { // blk is head block of a macro (only head blks are solved) - remaining.emplace(place_macros.macros()[place_macros.get_imacro_from_iblk(blk)].members.size(), blk); + remaining.emplace(place_macros[place_macros.get_imacro_from_iblk(blk)].members.size(), blk); } else { remaining.emplace(1, blk); } @@ -1146,7 +1146,7 @@ bool CutSpreader::try_place_macro(ClusterBlockId blk, targets.emplace_back(visit_blk, target); if (place_macros.macro_head(visit_blk) == visit_blk) { // if visit_blk is the head block of the macro // push all macro members to visit queue along with their calculated positions - const std::vector& members = place_macros.macros()[place_macros.get_imacro_from_iblk(blk)].members; + const std::vector& members = place_macros[place_macros.get_imacro_from_iblk(blk)].members; for (auto member = members.begin() + 1; member != members.end(); ++member) { t_pl_loc mloc = target + member->offset; // calculate member_loc using (head blk location + offset) visit.emplace(member->blk_index, mloc); diff --git a/vpr/src/place/initial_placement.cpp b/vpr/src/place/initial_placement.cpp index 4e0e13ded1c..2344efbdd40 100644 --- a/vpr/src/place/initial_placement.cpp +++ b/vpr/src/place/initial_placement.cpp @@ -1185,7 +1185,7 @@ bool place_one_block(const ClusterBlockId blk_id, if (imacro != -1) { //If the block belongs to a macro, pass that macro to the placement routines VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\tBelongs to a macro %d\n", imacro); - const t_pl_macro& pl_macro = place_macros.macros()[imacro]; + const t_pl_macro& pl_macro = place_macros[imacro]; placed_macro = place_macro(MAX_NUM_TRIES_TO_PLACE_MACROS_RANDOMLY, pl_macro, pad_loc_type, blk_types_empty_locs_in_grid, *block_scores, blk_loc_registry); } else { //If it does not belong to a macro, create a macro with the one block and then pass to the placement routines diff --git a/vpr/src/place/move_utils.cpp b/vpr/src/place/move_utils.cpp index 836511fb52f..835e2e799a4 100644 --- a/vpr/src/place/move_utils.cpp +++ b/vpr/src/place/move_utils.cpp @@ -101,7 +101,7 @@ e_block_move_result find_affected_blocks(t_pl_blocks_to_be_moved& blocks_affecte int imember_from = 0; outcome = record_macro_swaps(blocks_affected, imacro_from, imember_from, swap_offset, blk_loc_registry); - VTR_ASSERT_SAFE(outcome != e_block_move_result::VALID || imember_from == int(place_macros.macros()[imacro_from].members.size())); + VTR_ASSERT_SAFE(outcome != e_block_move_result::VALID || imember_from == int(place_macros[imacro_from].members.size())); } else { ClusterBlockId b_to = grid_blocks.block_at_location(to); @@ -186,10 +186,10 @@ e_block_move_result record_macro_swaps(t_pl_blocks_to_be_moved& blocks_affected, e_block_move_result outcome = e_block_move_result::VALID; - for (; imember_from < int(place_macros.macros()[imacro_from].members.size()) && outcome == e_block_move_result::VALID; imember_from++) { + for (; imember_from < int(place_macros[imacro_from].members.size()) && outcome == e_block_move_result::VALID; imember_from++) { // Gets the new from and to info for every block in the macro // cannot use the old from and to info - ClusterBlockId curr_b_from = place_macros.macros()[imacro_from].members[imember_from].blk_index; + ClusterBlockId curr_b_from = place_macros[imacro_from].members[imember_from].blk_index; t_pl_loc curr_from = block_locs[curr_b_from].loc; @@ -214,7 +214,7 @@ e_block_move_result record_macro_swaps(t_pl_blocks_to_be_moved& blocks_affected, if (imacro_from == imacro_to) { outcome = record_macro_self_swaps(blocks_affected, imacro_from, swap_offset, blk_loc_registry); - imember_from = place_macros.macros()[imacro_from].members.size(); + imember_from = place_macros[imacro_from].members.size(); break; //record_macro_self_swaps() handles this case completely, so we don't need to continue the loop } else { outcome = record_macro_macro_swaps(blocks_affected, imacro_from, imember_from, imacro_to, b_to, swap_offset, blk_loc_registry); @@ -349,7 +349,7 @@ e_block_move_result record_macro_move(t_pl_blocks_to_be_moved& blocks_affected, const auto& block_locs = blk_loc_registry.block_locs(); const GridBlock& grid_blocks = blk_loc_registry.grid_blocks(); - for (const t_pl_macro_member& member : place_macros.macros()[imacro].members) { + for (const t_pl_macro_member& member : place_macros[imacro].members) { t_pl_loc from = block_locs[member.blk_index].loc; t_pl_loc to = from + swap_offset; @@ -384,8 +384,8 @@ e_block_move_result identify_macro_self_swap_affected_macros(std::vector& m e_block_move_result outcome = e_block_move_result::VALID; - for (size_t imember = 0; imember < place_macros.macros()[imacro].members.size() && outcome == e_block_move_result::VALID; ++imember) { - ClusterBlockId blk = place_macros.macros()[imacro].members[imember].blk_index; + for (size_t imember = 0; imember < place_macros[imacro].members.size() && outcome == e_block_move_result::VALID; ++imember) { + ClusterBlockId blk = place_macros[imacro].members[imember].blk_index; t_pl_loc from = block_locs[blk].loc; t_pl_loc to = from + swap_offset; diff --git a/vpr/src/place/place_macro.cpp b/vpr/src/place/place_macro.cpp index 43f591feb43..9d091bc0f76 100644 --- a/vpr/src/place/place_macro.cpp +++ b/vpr/src/place/place_macro.cpp @@ -679,6 +679,14 @@ bool PlaceMacros::net_is_driven_by_direct_(ClusterNetId clb_net) { return direct != OPEN; } +//t_pl_macro& PlaceMacros::operator[](size_t idx) { +// return pl_macros_[idx]; +//} + +const t_pl_macro& PlaceMacros::operator[](int idx) const { + return pl_macros_[idx]; +} + static void validate_macros(const std::vector& macros) { diff --git a/vpr/src/place/place_macro.h b/vpr/src/place/place_macro.h index d275a486050..de3dedd9125 100644 --- a/vpr/src/place/place_macro.h +++ b/vpr/src/place/place_macro.h @@ -180,6 +180,9 @@ class PlaceMacros { const std::vector& macros() const; +// t_pl_macro& operator[](size_t idx); + const t_pl_macro& operator[](int idx) const; + private: /** From a8cabfaa05423de1a928fa48889d6865a6ff7001 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 1 Oct 2024 11:14:21 -0400 Subject: [PATCH 012/162] move get_coordinate_of_pin to BlkLocRegistry --- vpr/src/base/blk_loc_registry.cpp | 15 +++++++++++++++ vpr/src/base/blk_loc_registry.h | 8 ++++++++ vpr/src/place/analytic_placer.cpp | 2 -- vpr/src/place/directed_moves_util.cpp | 25 ++----------------------- vpr/src/place/directed_moves_util.h | 4 ---- vpr/src/place/initial_placement.cpp | 4 ++-- 6 files changed, 27 insertions(+), 31 deletions(-) diff --git a/vpr/src/base/blk_loc_registry.cpp b/vpr/src/base/blk_loc_registry.cpp index 814385473ad..9eaa7c0242e 100644 --- a/vpr/src/base/blk_loc_registry.cpp +++ b/vpr/src/base/blk_loc_registry.cpp @@ -215,3 +215,18 @@ void BlkLocRegistry::revert_move_blocks(const t_pl_blocks_to_be_moved& blocks_af expected_transaction_ = e_expected_transaction::APPLY; } + +t_physical_tile_loc BlkLocRegistry::get_coordinate_of_pin(ClusterPinId pin) const { + const auto& cluster_ctx = g_vpr_ctx.clustering(); + + int pnum = tile_pin_index(pin); + ClusterBlockId block = cluster_ctx.clb_nlist.pin_block(pin); + + t_physical_tile_loc tile_loc; + t_pl_loc block_loc = block_locs()[block].loc; + tile_loc.x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; + tile_loc.y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; + tile_loc.layer_num = block_loc.layer; + + return tile_loc; +} diff --git a/vpr/src/base/blk_loc_registry.h b/vpr/src/base/blk_loc_registry.h index c9ea2589cb0..70ce5afe7b6 100644 --- a/vpr/src/base/blk_loc_registry.h +++ b/vpr/src/base/blk_loc_registry.h @@ -115,6 +115,14 @@ class BlkLocRegistry { */ void revert_move_blocks(const t_pl_blocks_to_be_moved& blocks_affected); + ///@brief Helper function that returns the x, y coordinates of a pin + /** + * @brief Returns the coordinates of a cluster pin + * @param pin The unique Id of the cluster pin whose coordinates is desired. + * @return The coordinates of the given pin. + */ + t_physical_tile_loc get_coordinate_of_pin(ClusterPinId pin) const; + enum class e_expected_transaction { APPLY, COMMIT_REVERT diff --git a/vpr/src/place/analytic_placer.cpp b/vpr/src/place/analytic_placer.cpp index 95b43033322..20f6ce04dce 100644 --- a/vpr/src/place/analytic_placer.cpp +++ b/vpr/src/place/analytic_placer.cpp @@ -370,10 +370,8 @@ int AnalyticPlacer::total_hpwl() { */ void AnalyticPlacer::setup_solve_blks(t_logical_block_type_ptr blkTypes) { const ClusteredNetlist& clb_nlist = g_vpr_ctx.clustering().clb_nlist; - PlacementContext& place_ctx = g_vpr_ctx.mutable_placement(); const auto& place_macros = blk_loc_registry_ref_.place_macros(); - int row = 0; solve_blks.clear(); // clear row_num of all cells, so no blocks are solved diff --git a/vpr/src/place/directed_moves_util.cpp b/vpr/src/place/directed_moves_util.cpp index b8a950d832a..fce01118463 100644 --- a/vpr/src/place/directed_moves_util.cpp +++ b/vpr/src/place/directed_moves_util.cpp @@ -2,27 +2,6 @@ #include "directed_moves_util.h" #include "centroid_move_generator.h" -t_physical_tile_loc get_coordinate_of_pin(ClusterPinId pin, - const BlkLocRegistry& blk_loc_registry) { - const auto& device_ctx = g_vpr_ctx.device(); - const auto& grid = device_ctx.grid; - const auto& cluster_ctx = g_vpr_ctx.clustering(); - - int pnum = blk_loc_registry.tile_pin_index(pin); - ClusterBlockId block = cluster_ctx.clb_nlist.pin_block(pin); - - t_physical_tile_loc tile_loc; - t_pl_loc block_loc = blk_loc_registry.block_locs()[block].loc; - tile_loc.x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - tile_loc.y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - tile_loc.layer_num = block_loc.layer; - - tile_loc.x = std::max(std::min(tile_loc.x, (int)grid.width() - 2), 1); //-2 for no perim channels - tile_loc.y = std::max(std::min(tile_loc.y, (int)grid.height() - 2), 1); //-2 for no perim channels - - return tile_loc; -} - void calculate_centroid_loc(ClusterBlockId b_from, bool timing_weights, t_pl_loc& centroid, @@ -78,7 +57,7 @@ void calculate_centroid_loc(ClusterBlockId b_from, weight = 1; } - t_physical_tile_loc tile_loc = get_coordinate_of_pin(sink_pin_id, blk_loc_registry); + t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(sink_pin_id); acc_x += tile_loc.x * weight; acc_y += tile_loc.y * weight; @@ -98,7 +77,7 @@ void calculate_centroid_loc(ClusterBlockId b_from, ClusterPinId source_pin = cluster_ctx.clb_nlist.net_driver(net_id); - t_physical_tile_loc tile_loc = get_coordinate_of_pin(source_pin, blk_loc_registry); + t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(source_pin); acc_x += tile_loc.x * weight; acc_y += tile_loc.y * weight; diff --git a/vpr/src/place/directed_moves_util.h b/vpr/src/place/directed_moves_util.h index 4b78e424d92..5c23949907f 100644 --- a/vpr/src/place/directed_moves_util.h +++ b/vpr/src/place/directed_moves_util.h @@ -17,10 +17,6 @@ enum class e_reward_function { e_reward_function string_to_reward(const std::string& st); -///@brief Helper function that returns the x, y coordinates of a pin -t_physical_tile_loc get_coordinate_of_pin(ClusterPinId pin, - const BlkLocRegistry& blk_loc_registry); - /** * @brief Calculates the exact centroid location * diff --git a/vpr/src/place/initial_placement.cpp b/vpr/src/place/initial_placement.cpp index 2344efbdd40..f8e40afa895 100644 --- a/vpr/src/place/initial_placement.cpp +++ b/vpr/src/place/initial_placement.cpp @@ -448,7 +448,7 @@ static std::vector find_centroid_loc(const t_pl_macro& pl_macro, continue; } - t_physical_tile_loc tile_loc = get_coordinate_of_pin(sink_pin_id, blk_loc_registry); + t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(sink_pin_id); if (find_layer) { VTR_ASSERT(tile_loc.layer_num != OPEN); layer_count[tile_loc.layer_num]++; @@ -468,7 +468,7 @@ static std::vector find_centroid_loc(const t_pl_macro& pl_macro, continue; } - t_physical_tile_loc tile_loc = get_coordinate_of_pin(source_pin, blk_loc_registry); + t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(source_pin); if (find_layer) { VTR_ASSERT(tile_loc.layer_num != OPEN); layer_count[tile_loc.layer_num]++; From 1fc28dba93e3b6ecfb5c8f7a97987e0a4838f408 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 1 Oct 2024 14:11:07 -0400 Subject: [PATCH 013/162] call get_coordinate_of_pin() instead of computing the pin location in place --- vpr/src/place/net_cost_handler.cpp | 275 ++++++++++++----------------- 1 file changed, 110 insertions(+), 165 deletions(-) diff --git a/vpr/src/place/net_cost_handler.cpp b/vpr/src/place/net_cost_handler.cpp index 1dbef60a546..5365d5b22a6 100644 --- a/vpr/src/place/net_cost_handler.cpp +++ b/vpr/src/place/net_cost_handler.cpp @@ -53,9 +53,6 @@ constexpr std::array cross_count = {1.0000, 1. 2.5610, 2.5864, 2.6117, 2.6371, 2.6625, 2.6887, 2.7148, 2.7410, 2.7671, 2.7933}; - - - /** * @brief If the moving pin is of type type SINK, update bb_pin_sink_count_new which stores the number of sink pins on each layer of "net_id" * @param pin_old_loc Old location of the moving pin @@ -87,8 +84,6 @@ static void add_block_to_bb(const t_physical_tile_loc& new_pin_loc, t_2D_bb& bb_edge_new, t_2D_bb& bb_coord_new); - - /** * @brief Given the 3D BB, calculate the wire-length estimate of the net * @param net_id ID of the net which wirelength estimate is requested @@ -104,8 +99,6 @@ static double get_net_wirelength_estimate(ClusterNetId net_id, const t_bb& bb); */ static double wirelength_crossing_count(size_t fanout); - - /******************************* End of Function definitions ************************************/ @@ -488,7 +481,7 @@ void NetCostHandler::get_non_updatable_cube_bb_(ClusterNetId net_id, bool use_ts //TODO: account for multiple physical pin instances per logical pin const auto& cluster_ctx = g_vpr_ctx.clustering(); const auto& device_ctx = g_vpr_ctx.device(); - const auto& block_locs = placer_state_.block_locs(); + const auto& blk_loc_registry = placer_state_.blk_loc_registry(); auto& move_ctx = placer_state_.mutable_move(); // the bounding box coordinates that is going to be updated by this function @@ -496,60 +489,52 @@ void NetCostHandler::get_non_updatable_cube_bb_(ClusterNetId net_id, bool use_ts // the number of sink pins of "net_id" on each layer vtr::NdMatrixProxy num_sink_pin_layer = use_ts ? ts_layer_sink_pin_count_[size_t(net_id)] : move_ctx.num_sink_pin_layer[size_t(net_id)]; - ClusterBlockId bnum = cluster_ctx.clb_nlist.net_driver_block(net_id); - int pnum = placer_state_.blk_loc_registry().net_pin_to_tile_pin_index(net_id, 0); + // get the source pin's location + ClusterPinId source_pin_id = cluster_ctx.clb_nlist.net_pin(net_id, 0); + t_physical_tile_loc source_pin_loc = blk_loc_registry.get_coordinate_of_pin(source_pin_id); - t_pl_loc block_loc = block_locs[bnum].loc; - int x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - int layer = block_loc.layer; - - bb_coord_new.xmin = x; - bb_coord_new.ymin = y; - bb_coord_new.layer_min = layer; - bb_coord_new.xmax = x; - bb_coord_new.ymax = y; - bb_coord_new.layer_max = layer; + // initialize the bounding box coordinates with the source pin's coordinates + bb_coord_new.xmin = source_pin_loc.x; + bb_coord_new.ymin = source_pin_loc.y; + bb_coord_new.layer_min = source_pin_loc.layer_num; + bb_coord_new.xmax = source_pin_loc.x; + bb_coord_new.ymax = source_pin_loc.y; + bb_coord_new.layer_max = source_pin_loc.layer_num; for (int layer_num = 0; layer_num < device_ctx.grid.get_num_layers(); layer_num++) { num_sink_pin_layer[layer_num] = 0; } for (ClusterPinId pin_id : cluster_ctx.clb_nlist.net_sinks(net_id)) { - bnum = cluster_ctx.clb_nlist.pin_block(pin_id); - block_loc = block_locs[bnum].loc; - pnum = placer_state_.blk_loc_registry().tile_pin_index(pin_id); - x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - layer = block_loc.layer; - - if (x < bb_coord_new.xmin) { - bb_coord_new.xmin = x; - } else if (x > bb_coord_new.xmax) { - bb_coord_new.xmax = x; + t_physical_tile_loc pin_loc = blk_loc_registry.get_coordinate_of_pin(pin_id); + + if (pin_loc.x < bb_coord_new.xmin) { + bb_coord_new.xmin = pin_loc.x; + } else if (pin_loc.x > bb_coord_new.xmax) { + bb_coord_new.xmax = pin_loc.x; } - if (y < bb_coord_new.ymin) { - bb_coord_new.ymin = y; - } else if (y > bb_coord_new.ymax) { - bb_coord_new.ymax = y; + if (pin_loc.y < bb_coord_new.ymin) { + bb_coord_new.ymin = pin_loc.y; + } else if (pin_loc.y > bb_coord_new.ymax) { + bb_coord_new.ymax = pin_loc.y; } - if (layer < bb_coord_new.layer_min) { - bb_coord_new.layer_min = layer; - } else if (layer > bb_coord_new.layer_max) { - bb_coord_new.layer_max = layer; + if (pin_loc.layer_num < bb_coord_new.layer_min) { + bb_coord_new.layer_min = pin_loc.layer_num; + } else if (pin_loc.layer_num > bb_coord_new.layer_max) { + bb_coord_new.layer_max = pin_loc.layer_num; } - num_sink_pin_layer[layer]++; + num_sink_pin_layer[pin_loc.layer_num]++; } } void NetCostHandler::get_non_updatable_per_layer_bb_(ClusterNetId net_id, bool use_ts) { //TODO: account for multiple physical pin instances per logical pin - auto& device_ctx = g_vpr_ctx.device(); - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& block_locs = placer_state_.block_locs(); + const auto& device_ctx = g_vpr_ctx.device(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& blk_loc_registry = placer_state_.blk_loc_registry(); auto& move_ctx = placer_state_.mutable_move(); std::vector& bb_coord_new = use_ts ? layer_ts_bb_coord_new_[net_id] : move_ctx.layer_bb_coords[net_id]; @@ -558,38 +543,29 @@ void NetCostHandler::get_non_updatable_per_layer_bb_(ClusterNetId net_id, bool u const int num_layers = device_ctx.grid.get_num_layers(); VTR_ASSERT_DEBUG(bb_coord_new.size() == (size_t)num_layers); - ClusterBlockId bnum = cluster_ctx.clb_nlist.net_driver_block(net_id); - t_pl_loc block_loc = block_locs[bnum].loc; - int pnum = placer_state_.blk_loc_registry().net_pin_to_tile_pin_index(net_id, 0); - - int src_x = block_locs[bnum].loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int src_y = block_locs[bnum].loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; + // get the source pin's location + ClusterPinId source_pin_id = cluster_ctx.clb_nlist.net_pin(net_id, 0); + t_physical_tile_loc source_pin_loc = blk_loc_registry.get_coordinate_of_pin(source_pin_id); for (int layer_num = 0; layer_num < num_layers; layer_num++) { - bb_coord_new[layer_num] = t_2D_bb{src_x, src_x, src_y, src_y, layer_num}; + bb_coord_new[layer_num] = t_2D_bb{source_pin_loc.x, source_pin_loc.x, source_pin_loc.y, source_pin_loc.y, source_pin_loc.layer_num}; num_sink_layer[layer_num] = 0; } for (ClusterPinId pin_id : cluster_ctx.clb_nlist.net_sinks(net_id)) { - bnum = cluster_ctx.clb_nlist.pin_block(pin_id); - block_loc = block_locs[bnum].loc; - pnum = placer_state_.blk_loc_registry().tile_pin_index(pin_id); - int x = block_locs[bnum].loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int y = block_locs[bnum].loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - - int layer_num = block_locs[bnum].loc.layer; - num_sink_layer[layer_num]++; - - if (x < bb_coord_new[layer_num].xmin) { - bb_coord_new[layer_num].xmin = x; - } else if (x > bb_coord_new[layer_num].xmax) { - bb_coord_new[layer_num].xmax = x; + t_physical_tile_loc pin_loc = blk_loc_registry.get_coordinate_of_pin(pin_id); + num_sink_layer[pin_loc.layer_num]++; + + if (pin_loc.x < bb_coord_new[pin_loc.layer_num].xmin) { + bb_coord_new[pin_loc.layer_num].xmin = pin_loc.x; + } else if (pin_loc.x > bb_coord_new[pin_loc.layer_num].xmax) { + bb_coord_new[pin_loc.layer_num].xmax = pin_loc.x; } - if (y < bb_coord_new[layer_num].ymin) { - bb_coord_new[layer_num].ymin = y; - } else if (y > bb_coord_new[layer_num].ymax) { - bb_coord_new[layer_num].ymax = y; + if (pin_loc.y < bb_coord_new[pin_loc.layer_num].ymin) { + bb_coord_new[pin_loc.layer_num].ymin = pin_loc.y; + } else if (pin_loc.y > bb_coord_new[pin_loc.layer_num].ymax) { + bb_coord_new[pin_loc.layer_num].ymax = pin_loc.y; } } } @@ -1208,26 +1184,22 @@ void NetCostHandler::get_bb_from_scratch_(ClusterNetId net_id, t_bb& coords, t_bb& num_on_edges, vtr::NdMatrixProxy num_sink_pin_layer) { - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& device_ctx = g_vpr_ctx.device(); - auto& grid = device_ctx.grid; - auto& block_locs = placer_state_.block_locs(); - - ClusterBlockId bnum = cluster_ctx.clb_nlist.net_driver_block(net_id); - t_pl_loc block_loc = block_locs[bnum].loc; - int pnum = placer_state_.blk_loc_registry().net_pin_to_tile_pin_index(net_id, 0); + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& device_ctx = g_vpr_ctx.device(); + const auto& grid = device_ctx.grid; + const auto& block_locs = placer_state_.block_locs(); + const auto& blk_loc_registry = placer_state_.blk_loc_registry(); - VTR_ASSERT_SAFE(pnum >= 0); - int x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - int pin_layer = block_loc.layer; + // get the source pin's location + ClusterPinId source_pin_id = cluster_ctx.clb_nlist.net_pin(net_id, 0); + t_physical_tile_loc source_pin_loc = blk_loc_registry.get_coordinate_of_pin(source_pin_id); - int xmin = x; - int ymin = y; - int layer_min = pin_layer; - int xmax = x; - int ymax = y; - int layer_max = pin_layer; + int xmin = source_pin_loc.x; + int ymin = source_pin_loc.y; + int layer_min = source_pin_loc.layer_num; + int xmax = source_pin_loc.x; + int ymax = source_pin_loc.y; + int layer_max = source_pin_loc.layer_num; int xmin_edge = 1; int ymin_edge = 1; @@ -1241,60 +1213,48 @@ void NetCostHandler::get_bb_from_scratch_(ClusterNetId net_id, } for (ClusterPinId pin_id : cluster_ctx.clb_nlist.net_sinks(net_id)) { - bnum = cluster_ctx.clb_nlist.pin_block(pin_id); - block_loc = block_locs[bnum].loc; - pnum = placer_state_.blk_loc_registry().tile_pin_index(pin_id); - x = block_locs[bnum].loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - y = block_locs[bnum].loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - pin_layer = block_locs[bnum].loc.layer; - - /* Code below counts IO blocks as being within the 1..grid.width()-2, 1..grid.height()-2 clb array. * - * This is because channels do not go out of the 0..grid.width()-2, 0..grid.height()-2 range, and * - * I always take all channels impinging on the bounding box to be within * - * that bounding box. Hence, this "movement" of IO blocks does not affect * - * the which channels are included within the bounding box, and it * - * simplifies the code a lot. */ - - if (x == xmin) { + t_physical_tile_loc pin_loc = blk_loc_registry.get_coordinate_of_pin(pin_id); + + if (pin_loc.x == xmin) { xmin_edge++; } - if (x == xmax) { /* Recall that xmin could equal xmax -- don't use else */ + if (pin_loc.x == xmax) { /* Recall that xmin could equal xmax -- don't use else */ xmax_edge++; - } else if (x < xmin) { - xmin = x; + } else if (pin_loc.x < xmin) { + xmin = pin_loc.x; xmin_edge = 1; - } else if (x > xmax) { - xmax = x; + } else if (pin_loc.x > xmax) { + xmax = pin_loc.x; xmax_edge = 1; } - if (y == ymin) { + if (pin_loc.y == ymin) { ymin_edge++; } - if (y == ymax) { + if (pin_loc.y == ymax) { ymax_edge++; - } else if (y < ymin) { - ymin = y; + } else if (pin_loc.y < ymin) { + ymin = pin_loc.y; ymin_edge = 1; - } else if (y > ymax) { - ymax = y; + } else if (pin_loc.y > ymax) { + ymax = pin_loc.y; ymax_edge = 1; } - if (pin_layer == layer_min) { + if (pin_loc.layer_num == layer_min) { layer_min_edge++; } - if (pin_layer == layer_max) { + if (pin_loc.layer_num == layer_max) { layer_max_edge++; - } else if (pin_layer < layer_min) { - layer_min = pin_layer; + } else if (pin_loc.layer_num < layer_min) { + layer_min = pin_loc.layer_num; layer_min_edge = 1; - } else if (pin_layer > layer_max) { - layer_max = pin_layer; + } else if (pin_loc.layer_num > layer_max) { + layer_max = pin_loc.layer_num; layer_max_edge = 1; } - num_sink_pin_layer[pin_layer]++; + num_sink_pin_layer[pin_loc.layer_num]++; } // Copy the coordinates and number on edges information into the proper structures. @@ -1319,71 +1279,56 @@ void NetCostHandler::get_layer_bb_from_scratch_(ClusterNetId net_id, std::vector& num_on_edges, std::vector& coords, vtr::NdMatrixProxy layer_pin_sink_count) { - auto& device_ctx = g_vpr_ctx.device(); - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& block_locs = placer_state_.block_locs(); + const auto& device_ctx = g_vpr_ctx.device(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& blk_loc_registry = placer_state_.blk_loc_registry(); const int num_layers = device_ctx.grid.get_num_layers(); VTR_ASSERT_DEBUG(coords.size() == (size_t)num_layers); VTR_ASSERT_DEBUG(num_on_edges.size() == (size_t)num_layers); - ClusterBlockId bnum = cluster_ctx.clb_nlist.net_driver_block(net_id); - t_pl_loc block_loc = block_locs[bnum].loc; - int pnum_src = placer_state_.blk_loc_registry().net_pin_to_tile_pin_index(net_id, 0); - VTR_ASSERT_SAFE(pnum_src >= 0); - int x_src = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum_src]; - int y_src = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum_src]; + // get the source pin's location + ClusterPinId source_pin_id = cluster_ctx.clb_nlist.net_pin(net_id, 0); + t_physical_tile_loc source_pin_loc = blk_loc_registry.get_coordinate_of_pin(source_pin_id); // TODO: Currently we are assuming that crossing can only happen from OPIN. Because of that, // when per-layer bounding box is used, we want the bounding box on each layer to also include // the location of source since the connection on each layer starts from that location. for (int layer_num = 0; layer_num < num_layers; layer_num++) { - coords[layer_num] = t_2D_bb{x_src, x_src, y_src, y_src, layer_num}; + coords[layer_num] = t_2D_bb{source_pin_loc.x, source_pin_loc.x, source_pin_loc.y, source_pin_loc.y, source_pin_loc.layer_num}; num_on_edges[layer_num] = t_2D_bb{1, 1, 1, 1, layer_num}; layer_pin_sink_count[layer_num] = 0; } for (ClusterPinId pin_id : cluster_ctx.clb_nlist.net_sinks(net_id)) { - bnum = cluster_ctx.clb_nlist.pin_block(pin_id); - block_loc = block_locs[bnum].loc; - int pnum = placer_state_.blk_loc_registry().tile_pin_index(pin_id); - int layer = block_locs[bnum].loc.layer; - VTR_ASSERT_SAFE(layer >= 0 && layer < num_layers); - layer_pin_sink_count[layer]++; - int x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - - /* Code below counts IO blocks as being within the 1..grid.width()-2, 1..grid.height()-2 clb array. * - * This is because channels do not go out of the 0..grid.width()-2, 0..grid.height()-2 range, and * - * I always take all channels impinging on the bounding box to be within * - * that bounding box. Hence, this "movement" of IO blocks does not affect * - * the which channels are included within the bounding box, and it * - * simplifies the code a lot. */ - - if (x == coords[layer].xmin) { - num_on_edges[layer].xmin++; + t_physical_tile_loc pin_loc = blk_loc_registry.get_coordinate_of_pin(pin_id); + VTR_ASSERT_SAFE(pin_loc.layer_num >= 0 && pin_loc.layer_num < num_layers); + layer_pin_sink_count[pin_loc.layer_num]++; + + if (pin_loc.x == coords[pin_loc.layer_num].xmin) { + num_on_edges[pin_loc.layer_num].xmin++; } - if (x == coords[layer].xmax) { /* Recall that xmin could equal xmax -- don't use else */ - num_on_edges[layer].xmax++; - } else if (x < coords[layer].xmin) { - coords[layer].xmin = x; - num_on_edges[layer].xmin = 1; - } else if (x > coords[layer].xmax) { - coords[layer].xmax = x; - num_on_edges[layer].xmax = 1; + if (pin_loc.x == coords[pin_loc.layer_num].xmax) { /* Recall that xmin could equal xmax -- don't use else */ + num_on_edges[pin_loc.layer_num].xmax++; + } else if (pin_loc.x < coords[pin_loc.layer_num].xmin) { + coords[pin_loc.layer_num].xmin = pin_loc.x; + num_on_edges[pin_loc.layer_num].xmin = 1; + } else if (pin_loc.x > coords[pin_loc.layer_num].xmax) { + coords[pin_loc.layer_num].xmax = pin_loc.x; + num_on_edges[pin_loc.layer_num].xmax = 1; } - if (y == coords[layer].ymin) { - num_on_edges[layer].ymin++; + if (pin_loc.y == coords[pin_loc.layer_num].ymin) { + num_on_edges[pin_loc.layer_num].ymin++; } - if (y == coords[layer].ymax) { - num_on_edges[layer].ymax++; - } else if (y < coords[layer].ymin) { - coords[layer].ymin = y; - num_on_edges[layer].ymin = 1; - } else if (y > coords[layer].ymax) { - coords[layer].ymax = y; - num_on_edges[layer].ymax = 1; + if (pin_loc.y == coords[pin_loc.layer_num].ymax) { + num_on_edges[pin_loc.layer_num].ymax++; + } else if (pin_loc.y < coords[pin_loc.layer_num].ymin) { + coords[pin_loc.layer_num].ymin = pin_loc.y; + num_on_edges[pin_loc.layer_num].ymin = 1; + } else if (pin_loc.y > coords[pin_loc.layer_num].ymax) { + coords[pin_loc.layer_num].ymax = pin_loc.y; + num_on_edges[pin_loc.layer_num].ymax = 1; } } } From 09421bd54fb05a6736ecb951e554d5402da4cad7 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 1 Oct 2024 15:01:02 -0400 Subject: [PATCH 014/162] move calculate_centroid_loc() to CentroidMoveGenerator --- vpr/src/place/RL_agent_util.cpp | 2 +- vpr/src/place/centroid_move_generator.cpp | 124 +++++++++++++++++- vpr/src/place/centroid_move_generator.h | 30 ++++- vpr/src/place/directed_moves_util.cpp | 113 ---------------- vpr/src/place/directed_moves_util.h | 38 ------ vpr/src/place/net_cost_handler.cpp | 43 +++--- .../weighted_centroid_move_generator.cpp | 66 +--------- .../place/weighted_centroid_move_generator.h | 13 +- 8 files changed, 172 insertions(+), 257 deletions(-) diff --git a/vpr/src/place/RL_agent_util.cpp b/vpr/src/place/RL_agent_util.cpp index 19ed06edd9c..e418a7db6ee 100644 --- a/vpr/src/place/RL_agent_util.cpp +++ b/vpr/src/place/RL_agent_util.cpp @@ -31,7 +31,7 @@ std::pair, std::unique_ptr> create * - 1st state: includes 4 moves (Uniform / Median / Centroid / * * WeightedCentroid) * * If agent should propose block type as well as the mentioned * - * move types, 1st state Q-table size is: * + * move types, 1st state Q-table size is: * * 4 move types * number of block types in the netlist * * if not, the Q-table size is : 4 * * * diff --git a/vpr/src/place/centroid_move_generator.cpp b/vpr/src/place/centroid_move_generator.cpp index 7fafa7743f8..fe76ff224a3 100644 --- a/vpr/src/place/centroid_move_generator.cpp +++ b/vpr/src/place/centroid_move_generator.cpp @@ -19,7 +19,8 @@ std::map CentroidMoveGenerator::noc_router_to_noc_gr CentroidMoveGenerator::CentroidMoveGenerator(PlacerState& placer_state, e_reward_function reward_function) : MoveGenerator(placer_state, reward_function) - , noc_attraction_w_(0.0f) + , weighted_(false) + , noc_attraction_weight_(0.0f) , noc_attraction_enabled_(false) {} CentroidMoveGenerator::CentroidMoveGenerator(PlacerState& placer_state, @@ -27,7 +28,7 @@ CentroidMoveGenerator::CentroidMoveGenerator(PlacerState& placer_state, float noc_attraction_weight, size_t high_fanout_net) : MoveGenerator(placer_state, reward_function) - , noc_attraction_w_(noc_attraction_weight) + , noc_attraction_weight_(noc_attraction_weight) , noc_attraction_enabled_(true) { VTR_ASSERT(noc_attraction_weight > 0.0 && noc_attraction_weight <= 1.0); @@ -45,7 +46,7 @@ e_create_move CentroidMoveGenerator::propose_move(t_pl_blocks_to_be_moved& block t_propose_action& proposed_action, float rlim, const t_placer_opts& placer_opts, - const PlacerCriticalities* /*criticalities*/) { + const PlacerCriticalities* criticalities) { auto& placer_state = placer_state_.get(); const auto& block_locs = placer_state.block_locs(); const auto& device_ctx = g_vpr_ctx.device(); @@ -72,8 +73,6 @@ e_create_move CentroidMoveGenerator::propose_move(t_pl_blocks_to_be_moved& block return e_create_move::ABORT; } - - t_pl_loc from = block_locs[b_from].loc; t_logical_block_type_ptr cluster_from_type = cluster_ctx.clb_nlist.block_type(b_from); t_physical_tile_type_ptr grid_from_type = device_ctx.grid.get_physical_type({from.x, from.y, from.layer}); @@ -83,10 +82,10 @@ e_create_move CentroidMoveGenerator::propose_move(t_pl_blocks_to_be_moved& block place_move_ctx.first_rlim, placer_opts.place_dm_rlim}; - t_pl_loc to, centroid; + t_pl_loc to; /* Calculate the centroid location*/ - calculate_centroid_loc(b_from, false, centroid, nullptr, noc_attraction_enabled_, noc_attraction_w_, blk_loc_registry); + t_pl_loc centroid = calculate_centroid_loc_(b_from, weighted_ ? criticalities : nullptr); // Centroid location is not necessarily a valid location, and the downstream location expects a valid // layer for the centroid location. So if the layer is not valid, we set it to the same layer as from loc. @@ -212,3 +211,114 @@ void CentroidMoveGenerator::initialize_noc_groups(size_t high_fanout_net) { } } } + +t_pl_loc CentroidMoveGenerator::calculate_centroid_loc_(ClusterBlockId b_from, + const PlacerCriticalities* criticalities) { + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& blk_loc_registry = placer_state_.get().blk_loc_registry(); + const auto& block_locs = blk_loc_registry.block_locs(); + + float acc_weight = 0; + float acc_x = 0; + float acc_y = 0; + float acc_layer = 0; + float weight = 1; + + int from_block_layer_num = block_locs[b_from].loc.layer; + VTR_ASSERT(from_block_layer_num != OPEN); + + //iterate over the from block pins + for (ClusterPinId pin_id : cluster_ctx.clb_nlist.block_pins(b_from)) { + ClusterNetId net_id = cluster_ctx.clb_nlist.pin_net(pin_id); + + if (cluster_ctx.clb_nlist.net_is_ignored(net_id)) { + continue; + } + + /* Ignore the special case nets which only connects a block to itself * + * Experimentally, it was found that this case greatly degrade QoR */ + if (cluster_ctx.clb_nlist.net_sinks(net_id).size() == 1) { + ClusterBlockId source = cluster_ctx.clb_nlist.net_driver_block(net_id); + ClusterPinId sink_pin = *cluster_ctx.clb_nlist.net_sinks(net_id).begin(); + ClusterBlockId sink = cluster_ctx.clb_nlist.pin_block(sink_pin); + if (sink == source) { + continue; + } + } + + //if the pin is driver iterate over all the sinks + if (cluster_ctx.clb_nlist.pin_type(pin_id) == PinType::DRIVER) { + if (cluster_ctx.clb_nlist.net_is_ignored(net_id)) + continue; + + for (auto sink_pin_id : cluster_ctx.clb_nlist.net_sinks(net_id)) { + /* Ignore if one of the sinks is the block itself * + * This case rarely happens but causes QoR degradation */ + if (pin_id == sink_pin_id) + continue; + int ipin = cluster_ctx.clb_nlist.pin_net_index(sink_pin_id); + if (criticalities != nullptr) { + weight = criticalities->criticality(net_id, ipin); + } else { + weight = 1; + } + + t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(sink_pin_id); + + acc_x += tile_loc.x * weight; + acc_y += tile_loc.y * weight; + acc_layer += tile_loc.layer_num * weight; + acc_weight += weight; + } + } + + //else the pin is sink --> only care about its driver + else { + int ipin = cluster_ctx.clb_nlist.pin_net_index(pin_id); + if (criticalities != nullptr) { + weight = criticalities->criticality(net_id, ipin); + } else { + weight = 1; + } + + ClusterPinId source_pin = cluster_ctx.clb_nlist.net_driver(net_id); + + t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(source_pin); + + acc_x += tile_loc.x * weight; + acc_y += tile_loc.y * weight; + acc_layer += tile_loc.layer_num * weight; + acc_weight += weight; + } + } + + if (noc_attraction_enabled_) { + NocGroupId noc_grp_id = CentroidMoveGenerator::get_cluster_noc_group(b_from); + + // check if the block belongs to a NoC group + if (noc_grp_id != NocGroupId::INVALID()) { + // get the routers in the associated NoC group + const auto& noc_routers = CentroidMoveGenerator::get_noc_group_routers(noc_grp_id); + float single_noc_weight = (acc_weight * noc_attraction_weight_) / (float)noc_routers.size(); + + acc_x *= (1.0f - noc_attraction_weight_); + acc_y *= (1.0f - noc_attraction_weight_); + acc_weight *= (1.0f - noc_attraction_weight_); + + for (ClusterBlockId router_blk_id : noc_routers) { + t_block_loc router_loc = block_locs[router_blk_id]; + acc_x += router_loc.loc.x * single_noc_weight; + acc_y += router_loc.loc.y * single_noc_weight; + acc_weight += single_noc_weight; + } + } + } + + t_pl_loc centroid; + //Calculate the centroid location + centroid.x = (int)std::round(acc_x / acc_weight); + centroid.y = (int)std::round(acc_y / acc_weight); + centroid.layer = (int)std::round(acc_layer / acc_weight); + + return centroid; +} \ No newline at end of file diff --git a/vpr/src/place/centroid_move_generator.h b/vpr/src/place/centroid_move_generator.h index 01ee259cbc8..1a4af272e00 100644 --- a/vpr/src/place/centroid_move_generator.h +++ b/vpr/src/place/centroid_move_generator.h @@ -75,10 +75,38 @@ class CentroidMoveGenerator : public MoveGenerator { const t_placer_opts& placer_opts, const PlacerCriticalities* /*criticalities*/) override; + /** + * @brief Calculates the exact centroid location + * + * This function is very useful in centroid and weightedCentroid moves as it calculates + * the centroid location. It returns the calculated location in centroid. + * + * When NoC attraction is enabled, the computed centroid is slightly adjusted towards the location + * of NoC routers that are in the same NoC group b_from. + * + * @param b_from The block Id of the moving block + * @param timing_weights Determines whether to calculate centroid or + * weighted centroid location. If true, use the timing weights (weighted centroid). + * @param criticalities A pointer to the placer criticalities which is used when + * calculating weighted centroid (send a NULL pointer in case of centroid) + * @param noc_attraction_enabled Indicates whether the computed centroid location + * should be adjusted towards NoC routers in the NoC group of the given block. + * @param noc_attraction_weight When NoC attraction is enabled, this weight + * specifies to which extent the computed centroid should be adjusted. A value + * in range [0, 1] is expected. + * + * @return The calculated location is returned in centroid parameter that is sent by reference + */ + t_pl_loc calculate_centroid_loc_(ClusterBlockId b_from, + const PlacerCriticalities* criticalities); + + protected: + bool weighted_; + private: /** A value in range [0, 1] that specifies how much the centroid location * computation is biased towards the associated NoC routers*/ - float noc_attraction_w_; + float noc_attraction_weight_; /** Indicates whether the centroid calculation is NoC-biased.*/ bool noc_attraction_enabled_; diff --git a/vpr/src/place/directed_moves_util.cpp b/vpr/src/place/directed_moves_util.cpp index fce01118463..f1dbcea4906 100644 --- a/vpr/src/place/directed_moves_util.cpp +++ b/vpr/src/place/directed_moves_util.cpp @@ -1,118 +1,5 @@ #include "directed_moves_util.h" -#include "centroid_move_generator.h" - -void calculate_centroid_loc(ClusterBlockId b_from, - bool timing_weights, - t_pl_loc& centroid, - const PlacerCriticalities* criticalities, - bool noc_attraction_enabled, - float noc_attraction_weight, - const BlkLocRegistry& blk_loc_registry) { - const auto& cluster_ctx = g_vpr_ctx.clustering(); - const auto& block_locs = blk_loc_registry.block_locs(); - - float acc_weight = 0; - float acc_x = 0; - float acc_y = 0; - float acc_layer = 0; - float weight = 1; - - int from_block_layer_num = block_locs[b_from].loc.layer; - VTR_ASSERT(from_block_layer_num != OPEN); - - //iterate over the from block pins - for (ClusterPinId pin_id : cluster_ctx.clb_nlist.block_pins(b_from)) { - ClusterNetId net_id = cluster_ctx.clb_nlist.pin_net(pin_id); - - if (cluster_ctx.clb_nlist.net_is_ignored(net_id)) { - continue; - } - - /* Ignore the special case nets which only connects a block to itself * - * Experimentally, it was found that this case greatly degrade QoR */ - if (cluster_ctx.clb_nlist.net_sinks(net_id).size() == 1) { - ClusterBlockId source = cluster_ctx.clb_nlist.net_driver_block(net_id); - ClusterPinId sink_pin = *cluster_ctx.clb_nlist.net_sinks(net_id).begin(); - ClusterBlockId sink = cluster_ctx.clb_nlist.pin_block(sink_pin); - if (sink == source) { - continue; - } - } - - //if the pin is driver iterate over all the sinks - if (cluster_ctx.clb_nlist.pin_type(pin_id) == PinType::DRIVER) { - if (cluster_ctx.clb_nlist.net_is_ignored(net_id)) - continue; - - for (auto sink_pin_id : cluster_ctx.clb_nlist.net_sinks(net_id)) { - /* Ignore if one of the sinks is the block itself * - * This case rarely happens but causes QoR degradation */ - if (pin_id == sink_pin_id) - continue; - int ipin = cluster_ctx.clb_nlist.pin_net_index(sink_pin_id); - if (timing_weights) { - weight = criticalities->criticality(net_id, ipin); - } else { - weight = 1; - } - - t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(sink_pin_id); - - acc_x += tile_loc.x * weight; - acc_y += tile_loc.y * weight; - acc_layer += tile_loc.layer_num * weight; - acc_weight += weight; - } - } - - //else the pin is sink --> only care about its driver - else { - int ipin = cluster_ctx.clb_nlist.pin_net_index(pin_id); - if (timing_weights) { - weight = criticalities->criticality(net_id, ipin); - } else { - weight = 1; - } - - ClusterPinId source_pin = cluster_ctx.clb_nlist.net_driver(net_id); - - t_physical_tile_loc tile_loc = blk_loc_registry.get_coordinate_of_pin(source_pin); - - acc_x += tile_loc.x * weight; - acc_y += tile_loc.y * weight; - acc_layer += tile_loc.layer_num * weight; - acc_weight += weight; - } - } - - if (noc_attraction_enabled) { - NocGroupId noc_grp_id = CentroidMoveGenerator::get_cluster_noc_group(b_from); - - // check if the block belongs to a NoC group - if (noc_grp_id != NocGroupId::INVALID()) { - // get the routers in the associated NoC group - const auto& noc_routers = CentroidMoveGenerator::get_noc_group_routers(noc_grp_id); - float single_noc_weight = (acc_weight * noc_attraction_weight) / (float)noc_routers.size(); - - acc_x *= (1.0f - noc_attraction_weight); - acc_y *= (1.0f - noc_attraction_weight); - acc_weight *= (1.0f - noc_attraction_weight); - - for (ClusterBlockId router_blk_id : noc_routers) { - t_block_loc router_loc = block_locs[router_blk_id]; - acc_x += router_loc.loc.x * single_noc_weight; - acc_y += router_loc.loc.y * single_noc_weight; - acc_weight += single_noc_weight; - } - } - } - - //Calculate the centroid location - centroid.x = (int)std::round(acc_x / acc_weight); - centroid.y = (int)std::round(acc_y / acc_weight); - centroid.layer = (int)std::round(acc_layer / acc_weight); -} static std::map available_reward_function = { {"basic", e_reward_function::BASIC}, diff --git a/vpr/src/place/directed_moves_util.h b/vpr/src/place/directed_moves_util.h index 5c23949907f..c00666458f0 100644 --- a/vpr/src/place/directed_moves_util.h +++ b/vpr/src/place/directed_moves_util.h @@ -17,42 +17,4 @@ enum class e_reward_function { e_reward_function string_to_reward(const std::string& st); -/** - * @brief Calculates the exact centroid location - * - * This function is very useful in centroid and weightedCentroid moves as it calculates - * the centroid location. It returns the calculated location in centroid. - * - * When NoC attraction is enabled, the computed centroid is slightly adjusted towards the location - * of NoC routers that are in the same NoC group b_from. - * - * @param b_from The block Id of the moving block - * @param timing_weights Determines whether to calculate centroid or - * weighted centroid location. If true, use the timing weights (weighted centroid). - * @param criticalities A pointer to the placer criticalities which is used when - * calculating weighted centroid (send a NULL pointer in case of centroid) - * @param noc_attraction_enabled Indicates whether the computed centroid location - * should be adjusted towards NoC routers in the NoC group of the given block. - * @param noc_attraction_weight When NoC attraction is enabled, this weight - * specifies to which extent the computed centroid should be adjusted. A value - * in range [0, 1] is expected. - * - * @return The calculated location is returned in centroid parameter that is sent by reference - */ -void calculate_centroid_loc(ClusterBlockId b_from, - bool timing_weights, - t_pl_loc& centroid, - const PlacerCriticalities* criticalities, - bool noc_attraction_enabled, - float noc_attraction_weight, - const BlkLocRegistry& blk_loc_registry); - -inline void calculate_centroid_loc(ClusterBlockId b_from, - bool timing_weights, - t_pl_loc& centroid, - const PlacerCriticalities* criticalities, - const BlkLocRegistry& blk_loc_registry) { - calculate_centroid_loc(b_from, timing_weights, centroid, criticalities, false, 0.0f, blk_loc_registry); -} - #endif diff --git a/vpr/src/place/net_cost_handler.cpp b/vpr/src/place/net_cost_handler.cpp index 5365d5b22a6..b453f659847 100644 --- a/vpr/src/place/net_cost_handler.cpp +++ b/vpr/src/place/net_cost_handler.cpp @@ -229,7 +229,7 @@ double NetCostHandler::comp_bb_cost(e_cost_methods method) { } double NetCostHandler::comp_cube_bb_cost_(e_cost_methods method) { - auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); auto& place_move_ctx = placer_state_.mutable_move(); double cost = 0; @@ -266,7 +266,7 @@ double NetCostHandler::comp_cube_bb_cost_(e_cost_methods method) { } double NetCostHandler::comp_per_layer_bb_cost_(e_cost_methods method) { - auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); auto& place_move_ctx = placer_state_.mutable_move(); double cost = 0; @@ -369,8 +369,8 @@ void NetCostHandler::update_td_delta_costs_(const PlaceDelayModel* delay_model, * This is also done to minimize the number of timing node/edge invalidations * for incremental static timing analysis (incremental STA). */ - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& block_locs = placer_state_.block_locs(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& block_locs = placer_state_.block_locs(); const auto& connection_delay = placer_state_.timing().connection_delay; auto& connection_timing_cost = placer_state_.mutable_timing().connection_timing_cost; @@ -380,8 +380,7 @@ void NetCostHandler::update_td_delta_costs_(const PlaceDelayModel* delay_model, if (cluster_ctx.clb_nlist.pin_type(pin) == PinType::DRIVER) { /* This pin is a net driver on a moved block. */ /* Recompute all point to point connection delays for the net sinks. */ - for (size_t ipin = 1; ipin < cluster_ctx.clb_nlist.net_pins(net).size(); - ipin++) { + for (size_t ipin = 1; ipin < cluster_ctx.clb_nlist.net_pins(net).size(); ipin++) { float temp_delay = comp_td_single_connection_delay(delay_model, block_locs, net, ipin); /* If the delay hasn't changed, do not mark this pin as affected */ if (temp_delay == connection_delay[net][ipin]) { @@ -392,8 +391,7 @@ void NetCostHandler::update_td_delta_costs_(const PlaceDelayModel* delay_model, proposed_connection_delay[net][ipin] = temp_delay; proposed_connection_timing_cost[net][ipin] = criticalities.criticality(net, ipin) * temp_delay; - delta_timing_cost += proposed_connection_timing_cost[net][ipin] - - connection_timing_cost[net][ipin]; + delta_timing_cost += proposed_connection_timing_cost[net][ipin] - connection_timing_cost[net][ipin]; /* Record this connection in blocks_affected.affected_pins */ ClusterPinId sink_pin = cluster_ctx.clb_nlist.net_pin(net, ipin); @@ -886,22 +884,22 @@ void NetCostHandler::update_layer_bb_(ClusterNetId net_id, if (layer_changed) { update_bb_layer_changed_(net_id, - pin_old_loc, - pin_new_loc, - *curr_bb_edge, - *curr_bb_coord, - bb_pin_sink_count_new, - bb_edge_new, - bb_coord_new); + pin_old_loc, + pin_new_loc, + *curr_bb_edge, + *curr_bb_coord, + bb_pin_sink_count_new, + bb_edge_new, + bb_coord_new); } else { update_bb_same_layer_(net_id, - pin_old_loc, - pin_new_loc, - *curr_bb_edge, - *curr_bb_coord, - bb_pin_sink_count_new, - bb_edge_new, - bb_coord_new); + pin_old_loc, + pin_new_loc, + *curr_bb_edge, + *curr_bb_coord, + bb_pin_sink_count_new, + bb_edge_new, + bb_coord_new); } if (bb_update_status_[net_id] == NetUpdateState::NOT_UPDATED_YET) { @@ -1187,7 +1185,6 @@ void NetCostHandler::get_bb_from_scratch_(ClusterNetId net_id, const auto& cluster_ctx = g_vpr_ctx.clustering(); const auto& device_ctx = g_vpr_ctx.device(); const auto& grid = device_ctx.grid; - const auto& block_locs = placer_state_.block_locs(); const auto& blk_loc_registry = placer_state_.blk_loc_registry(); // get the source pin's location diff --git a/vpr/src/place/weighted_centroid_move_generator.cpp b/vpr/src/place/weighted_centroid_move_generator.cpp index b6c6f7683f8..60566ac9370 100644 --- a/vpr/src/place/weighted_centroid_move_generator.cpp +++ b/vpr/src/place/weighted_centroid_move_generator.cpp @@ -1,69 +1,7 @@ #include "weighted_centroid_move_generator.h" -#include "globals.h" -#include "directed_moves_util.h" -#include "place_constraints.h" -#include "placer_state.h" -#include "move_utils.h" - WeightedCentroidMoveGenerator::WeightedCentroidMoveGenerator(PlacerState& placer_state, e_reward_function reward_function) - : MoveGenerator(placer_state, reward_function) {} - -e_create_move WeightedCentroidMoveGenerator::propose_move(t_pl_blocks_to_be_moved& blocks_affected, - t_propose_action& proposed_action, - float rlim, - const t_placer_opts& placer_opts, - const PlacerCriticalities* criticalities) { - const auto& cluster_ctx = g_vpr_ctx.clustering(); - const auto& device_ctx = g_vpr_ctx.device(); - auto& placer_state = placer_state_.get(); - const auto& block_locs = placer_state.block_locs(); - auto& place_move_ctx = placer_state.mutable_move(); - const auto& blk_loc_registry = placer_state.blk_loc_registry(); - - //Find a movable block based on blk_type - ClusterBlockId b_from = propose_block_to_move(placer_opts, - proposed_action.logical_blk_type_index, - /*highly_crit_block=*/false, - /*net_from=*/nullptr, - /*pin_from=*/nullptr, - placer_state); - - VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "Weighted Centroid Move Choose Block %d - rlim %f\n", size_t(b_from), rlim); - - if (!b_from) { //No movable block found - VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\tNo movable block found\n"); - return e_create_move::ABORT; - } - - t_pl_loc from = block_locs[b_from].loc; - auto cluster_from_type = cluster_ctx.clb_nlist.block_type(b_from); - auto grid_from_type = device_ctx.grid.get_physical_type({from.x, from.y, from.layer}); - VTR_ASSERT(is_tile_compatible(grid_from_type, cluster_from_type)); - - t_range_limiters range_limiters{rlim, - place_move_ctx.first_rlim, - placer_opts.place_dm_rlim}; - - t_pl_loc to, centroid; - - /* Calculate the weighted centroid */ - calculate_centroid_loc(b_from, true, centroid, criticalities, blk_loc_registry); - - // Centroid location is not necessarily a valid location, and the downstream location expect a valid - // layer for "to" location. So if the layer is not valid, we set it to the same layer as from loc. - centroid.layer = (centroid.layer < 0) ? from.layer : centroid.layer; - if (!find_to_loc_centroid(cluster_from_type, from, centroid, range_limiters, to, b_from, blk_loc_registry)) { - return e_create_move::ABORT; - } - - e_create_move create_move = ::create_move(blocks_affected, b_from, to, blk_loc_registry); - - //Check that all the blocks affected by the move would still be in a legal floorplan region after the swap - if (!floorplan_legal(blocks_affected)) { - return e_create_move::ABORT; - } - - return create_move; + : CentroidMoveGenerator(placer_state, reward_function) { + weighted_ = true; } diff --git a/vpr/src/place/weighted_centroid_move_generator.h b/vpr/src/place/weighted_centroid_move_generator.h index 30ed146941c..f16d1747a44 100644 --- a/vpr/src/place/weighted_centroid_move_generator.h +++ b/vpr/src/place/weighted_centroid_move_generator.h @@ -1,7 +1,7 @@ #ifndef VPR_WEIGHTED_CENTROID_MOVE_GEN_H #define VPR_WEIGHTED_CENTROID_MOVE_GEN_H -#include "move_generator.h" -#include "timing_place.h" + +#include "centroid_move_generator.h" /** * @brief Weighted Centroid move generator @@ -12,18 +12,11 @@ * For more details, please refer to: * "Learn to Place: FPGA Placement using Reinforcement Learning and Directed Moves", ICFPT2020 */ -class WeightedCentroidMoveGenerator : public MoveGenerator { +class WeightedCentroidMoveGenerator : public CentroidMoveGenerator { public: WeightedCentroidMoveGenerator() = delete; WeightedCentroidMoveGenerator(PlacerState& placer_state, e_reward_function reward_function); - - private: - e_create_move propose_move(t_pl_blocks_to_be_moved& blocks_affected, - t_propose_action& proposed_action, - float rlim, - const t_placer_opts& placer_opts, - const PlacerCriticalities* criticalities) override; }; #endif From d37da141fed1ec4843580f2e718b6958a88a03e6 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 1 Oct 2024 16:41:45 -0400 Subject: [PATCH 015/162] added MoveAbortionLogger class --- vpr/src/place/move_transactions.cpp | 24 +++++++++- vpr/src/place/move_transactions.h | 15 ++++++ vpr/src/place/move_utils.cpp | 46 +++++-------------- vpr/src/place/move_utils.h | 9 +--- vpr/src/place/place.cpp | 2 +- vpr/src/place/timing_place_lookup.h | 1 + .../place/weighted_median_move_generator.cpp | 12 +---- 7 files changed, 55 insertions(+), 54 deletions(-) diff --git a/vpr/src/place/move_transactions.cpp b/vpr/src/place/move_transactions.cpp index a116be0d326..d51e0236cbf 100644 --- a/vpr/src/place/move_transactions.cpp +++ b/vpr/src/place/move_transactions.cpp @@ -21,7 +21,7 @@ e_block_move_result t_pl_blocks_to_be_moved::record_block_move(ClusterBlockId bl const BlkLocRegistry& blk_loc_registry) { auto [to_it, to_success] = moved_to.emplace(to); if (!to_success) { - log_move_abort("duplicate block move to location"); + move_abortion_logger.log_move_abort("duplicate block move to location"); return e_block_move_result::ABORT; } @@ -30,7 +30,7 @@ e_block_move_result t_pl_blocks_to_be_moved::record_block_move(ClusterBlockId bl auto [_, from_success] = moved_from.emplace(from); if (!from_success) { moved_to.erase(to_it); - log_move_abort("duplicate block move from location"); + move_abortion_logger.log_move_abort("duplicate block move from location"); return e_block_move_result::ABORT; } @@ -95,3 +95,23 @@ bool t_pl_blocks_to_be_moved::driven_by_moved_block(const ClusterNetId net) cons return is_driven_by_move_blk; } + +void MoveAbortionLogger::log_move_abort(std::string_view reason) { + auto it = move_abort_reasons_.find(reason); + if (it != move_abort_reasons_.end()) { + it->second++; + } else { + move_abort_reasons_.emplace(reason, 1); + } +} + +void MoveAbortionLogger::report_aborted_moves() const { + VTR_LOG("\n"); + VTR_LOG("Aborted Move Reasons:\n"); + if (move_abort_reasons_.empty()) { + VTR_LOG(" No moves aborted\n"); + } + for (const auto& kv : move_abort_reasons_) { + VTR_LOG(" %s: %zu\n", kv.first.c_str(), kv.second); + } +} diff --git a/vpr/src/place/move_transactions.h b/vpr/src/place/move_transactions.h index 68686be262e..363aaddd793 100644 --- a/vpr/src/place/move_transactions.h +++ b/vpr/src/place/move_transactions.h @@ -30,6 +30,19 @@ struct t_pl_moved_block { t_pl_loc new_loc; }; +class MoveAbortionLogger { + public: + /// Records a reasons for an aborted move. + void log_move_abort(std::string_view reason); + + /// Prints a brief report about aborted move reasons and counts. + void report_aborted_moves() const; + + private: + /// Records counts of reasons for aborted moves + std::map> move_abort_reasons_; +}; + /* Stores the list of cluster blocks to be moved in a swap during * * placement. * * Store the information on the blocks to be moved in a swap during * @@ -80,6 +93,8 @@ struct t_pl_blocks_to_be_moved { std::unordered_set moved_to; std::vector affected_pins; + + MoveAbortionLogger move_abortion_logger; }; #endif diff --git a/vpr/src/place/move_utils.cpp b/vpr/src/place/move_utils.cpp index 835e2e799a4..262a801c611 100644 --- a/vpr/src/place/move_utils.cpp +++ b/vpr/src/place/move_utils.cpp @@ -16,29 +16,6 @@ //Note: The flag is only effective if compiled with VTR_ENABLE_DEBUG_LOGGING bool f_placer_breakpoint_reached = false; -//Records counts of reasons for aborted moves -static std::map> f_move_abort_reasons; - -void log_move_abort(std::string_view reason) { - auto it = f_move_abort_reasons.find(reason); - if (it != f_move_abort_reasons.end()) { - it->second++; - } else { - f_move_abort_reasons.emplace(reason, 1); - } -} - -void report_aborted_moves() { - VTR_LOG("\n"); - VTR_LOG("Aborted Move Reasons:\n"); - if (f_move_abort_reasons.empty()) { - VTR_LOG(" No moves aborted\n"); - } - for (const auto& kv : f_move_abort_reasons) { - VTR_LOG(" %s: %zu\n", kv.first.c_str(), kv.second); - } -} - e_create_move create_move(t_pl_blocks_to_be_moved& blocks_affected, ClusterBlockId b_from, t_pl_loc to, @@ -53,7 +30,7 @@ e_create_move create_move(t_pl_blocks_to_be_moved& blocks_affected, ClusterBlockId b_to = grid_blocks.block_at_location(to); if (!b_to) { - log_move_abort("inverted move no to block"); + blocks_affected.move_abortion_logger.log_move_abort("inverted move no to block"); outcome = e_block_move_result::ABORT; } else { t_pl_loc from = block_locs[b_from].loc; @@ -61,7 +38,7 @@ e_create_move create_move(t_pl_blocks_to_be_moved& blocks_affected, outcome = find_affected_blocks(blocks_affected, b_to, from, blk_loc_registry); if (outcome == e_block_move_result::INVERT) { - log_move_abort("inverted move recursion"); + blocks_affected.move_abortion_logger.log_move_abort("inverted move recursion"); outcome = e_block_move_result::ABORT; } } @@ -203,7 +180,7 @@ e_block_move_result record_macro_swaps(t_pl_blocks_to_be_moved& blocks_affected, //Note that we need to explicitly check that the types match, since the device floorplan is not //(necessarily) translationally invariant for an arbitrary macro if (!is_legal_swap_to_location(curr_b_from, curr_to, blk_loc_registry)) { - log_move_abort("macro_from swap to location illegal"); + blocks_affected.move_abortion_logger.log_move_abort("macro_from swap to location illegal"); outcome = e_block_move_result::ABORT; } else { ClusterBlockId b_to = grid_blocks.block_at_location(curr_to); @@ -261,7 +238,7 @@ e_block_move_result record_macro_macro_swaps(t_pl_blocks_to_be_moved& blocks_aff int imember_to = 0; auto outcome = record_macro_swaps(blocks_affected, imacro_to, imember_to, -swap_offset, blk_loc_registry); if (outcome == e_block_move_result::INVERT) { - log_move_abort("invert recursion2"); + blocks_affected.move_abortion_logger.log_move_abort("invert recursion2"); outcome = e_block_move_result::ABORT; } else if (outcome == e_block_move_result::VALID) { outcome = e_block_move_result::INVERT_VALID; @@ -293,7 +270,7 @@ e_block_move_result record_macro_macro_swaps(t_pl_blocks_to_be_moved& blocks_aff ++imember_from, ++imember_to) { //Check that both macros have the same shape while they overlap if (pl_macros[imacro_from].members[imember_from].offset != pl_macros[imacro_to].members[imember_to].offset + from_to_macro_offset) { - log_move_abort("macro shapes disagree"); + blocks_affected.move_abortion_logger.log_move_abort("macro shapes disagree"); return e_block_move_result::ABORT; } @@ -313,7 +290,7 @@ e_block_move_result record_macro_macro_swaps(t_pl_blocks_to_be_moved& blocks_aff } if (!is_legal_swap_to_location(b_from, curr_to, blk_loc_registry)) { - log_move_abort("macro_from swap to location illegal"); + blocks_affected.move_abortion_logger.log_move_abort("macro_from swap to location illegal"); return e_block_move_result::ABORT; } @@ -355,7 +332,7 @@ e_block_move_result record_macro_move(t_pl_blocks_to_be_moved& blocks_affected, t_pl_loc to = from + swap_offset; if (!is_legal_swap_to_location(member.blk_index, to, blk_loc_registry)) { - log_move_abort("macro move to location illegal"); + blocks_affected.move_abortion_logger.log_move_abort("macro move to location illegal"); return e_block_move_result::ABORT; } @@ -377,7 +354,8 @@ e_block_move_result record_macro_move(t_pl_blocks_to_be_moved& blocks_affected, e_block_move_result identify_macro_self_swap_affected_macros(std::vector& macros, const int imacro, t_pl_offset swap_offset, - const BlkLocRegistry& blk_loc_registry) { + const BlkLocRegistry& blk_loc_registry, + MoveAbortionLogger& move_abortion_logger) { const auto& place_macros = blk_loc_registry.place_macros(); const auto& block_locs = blk_loc_registry.block_locs(); const GridBlock& grid_blocks = blk_loc_registry.grid_blocks(); @@ -391,7 +369,7 @@ e_block_move_result identify_macro_self_swap_affected_macros(std::vector& m t_pl_loc to = from + swap_offset; if (!is_legal_swap_to_location(blk, to, blk_loc_registry)) { - log_move_abort("macro move to location illegal"); + move_abortion_logger.log_move_abort("macro move to location illegal"); return e_block_move_result::ABORT; } @@ -403,7 +381,7 @@ e_block_move_result identify_macro_self_swap_affected_macros(std::vector& m auto itr = std::find(macros.begin(), macros.end(), imacro_to); if (itr == macros.end()) { macros.push_back(imacro_to); - outcome = identify_macro_self_swap_affected_macros(macros, imacro_to, swap_offset, blk_loc_registry); + outcome = identify_macro_self_swap_affected_macros(macros, imacro_to, swap_offset, blk_loc_registry, move_abortion_logger); } } } @@ -421,7 +399,7 @@ e_block_move_result record_macro_self_swaps(t_pl_blocks_to_be_moved& blocks_affe //Collect the macros affected std::vector affected_macros; - auto outcome = identify_macro_self_swap_affected_macros(affected_macros, imacro, swap_offset, blk_loc_registry); + auto outcome = identify_macro_self_swap_affected_macros(affected_macros, imacro, swap_offset, blk_loc_registry, blocks_affected.move_abortion_logger); if (outcome != e_block_move_result::VALID) { return outcome; diff --git a/vpr/src/place/move_utils.h b/vpr/src/place/move_utils.h index 80359dd07a2..0c221f89c4a 100644 --- a/vpr/src/place/move_utils.h +++ b/vpr/src/place/move_utils.h @@ -103,12 +103,6 @@ struct t_swap_stats { int num_ts_called = 0; }; -//Records a reasons for an aborted move -void log_move_abort(std::string_view reason); - -//Prints a breif report about aborted move reasons and counts -void report_aborted_moves(); - e_create_move create_move(t_pl_blocks_to_be_moved& blocks_affected, ClusterBlockId b_from, t_pl_loc to, @@ -155,7 +149,8 @@ e_block_move_result record_macro_move(t_pl_blocks_to_be_moved& blocks_affected, e_block_move_result identify_macro_self_swap_affected_macros(std::vector& macros, const int imacro, t_pl_offset swap_offset, - const BlkLocRegistry& blk_loc_registry); + const BlkLocRegistry& blk_loc_registry, + MoveAbortionLogger& move_abortion_logger); e_block_move_result record_macro_self_swaps(t_pl_blocks_to_be_moved& blocks_affected, const int imacro, diff --git a/vpr/src/place/place.cpp b/vpr/src/place/place.cpp index 286ed01759b..b4579fc951a 100644 --- a/vpr/src/place/place.cpp +++ b/vpr/src/place/place.cpp @@ -891,7 +891,7 @@ void try_place(const Netlist<>& net_list, //Some stats VTR_LOG("\n"); VTR_LOG("Swaps called: %d\n", swap_stats.num_ts_called); - report_aborted_moves(); + blocks_affected.move_abortion_logger.report_aborted_moves(); if (placer_opts.place_algorithm.is_timing_driven()) { //Final timing estimate diff --git a/vpr/src/place/timing_place_lookup.h b/vpr/src/place/timing_place_lookup.h index 865b2f44558..fba3f470483 100644 --- a/vpr/src/place/timing_place_lookup.h +++ b/vpr/src/place/timing_place_lookup.h @@ -12,6 +12,7 @@ std::unique_ptr compute_place_delay_model(const t_placer_opts& bool is_flat); std::vector get_best_classes(enum e_pin_type pintype, t_physical_tile_type_ptr type); + bool directconnect_exists(RRNodeId src_rr_node, RRNodeId sink_rr_node); #endif diff --git a/vpr/src/place/weighted_median_move_generator.cpp b/vpr/src/place/weighted_median_move_generator.cpp index 1e600defa31..b887e12b54e 100644 --- a/vpr/src/place/weighted_median_move_generator.cpp +++ b/vpr/src/place/weighted_median_move_generator.cpp @@ -40,11 +40,8 @@ e_create_move WeightedMedianMoveGenerator::propose_move(t_pl_blocks_to_be_moved& return e_create_move::ABORT; } - - int num_layers = g_vpr_ctx.device().grid.get_num_layers(); - t_pl_loc from = block_locs[b_from].loc; auto cluster_from_type = cluster_ctx.clb_nlist.block_type(b_from); auto grid_from_type = g_vpr_ctx.device().grid.get_physical_type({from.x, from.y, from.layer}); @@ -159,6 +156,8 @@ bool WeightedMedianMoveGenerator::get_bb_cost_for_net_excluding_block(ClusterNet t_bb_cost* coords) { const auto& blk_loc_registry = placer_state_.get().blk_loc_registry(); const auto& block_locs = blk_loc_registry.block_locs(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& device_ctx = g_vpr_ctx.device(); bool skip_net = true; @@ -176,10 +175,6 @@ bool WeightedMedianMoveGenerator::get_bb_cost_for_net_excluding_block(ClusterNet float layer_min_cost = 0.f; float layer_max_cost = 0.f; - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& device_ctx = g_vpr_ctx.device(); - auto& grid = device_ctx.grid; - bool is_first_block = true; for (ClusterPinId pin_id : cluster_ctx.clb_nlist.net_pins(net_id)) { ClusterBlockId bnum = cluster_ctx.clb_nlist.pin_block(pin_id); @@ -207,9 +202,6 @@ bool WeightedMedianMoveGenerator::get_bb_cost_for_net_excluding_block(ClusterNet int y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; int layer = block_loc.layer; - x = std::max(std::min(x, (int)grid.width() - 2), 1); //-2 for no perim channels - y = std::max(std::min(y, (int)grid.height() - 2), 1); //-2 for no perim channels - if (is_first_block) { xmin = x; xmin_cost = cost; From 6883744c70a0c2439a3f7ac7a715a4c421257fa6 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 1 Oct 2024 17:24:11 -0400 Subject: [PATCH 016/162] remove ditected_moves_util.h/cpp --- vpr/src/place/centroid_move_generator.cpp | 1 - vpr/src/place/directed_moves_util.cpp | 12 ----------- vpr/src/place/directed_moves_util.h | 20 ------------------- vpr/src/place/initial_placement.cpp | 4 ---- vpr/src/place/move_generator.cpp | 10 ++++++++++ vpr/src/place/move_generator.h | 14 ++++++++++++- vpr/src/place/simpleRL_move_generator.h | 2 +- .../place/weighted_median_move_generator.cpp | 1 - 8 files changed, 24 insertions(+), 40 deletions(-) delete mode 100644 vpr/src/place/directed_moves_util.cpp delete mode 100644 vpr/src/place/directed_moves_util.h diff --git a/vpr/src/place/centroid_move_generator.cpp b/vpr/src/place/centroid_move_generator.cpp index fe76ff224a3..a4b39fc1be9 100644 --- a/vpr/src/place/centroid_move_generator.cpp +++ b/vpr/src/place/centroid_move_generator.cpp @@ -1,7 +1,6 @@ #include "centroid_move_generator.h" #include "vpr_types.h" #include "globals.h" -#include "directed_moves_util.h" #include "place_constraints.h" #include "placer_state.h" #include "move_utils.h" diff --git a/vpr/src/place/directed_moves_util.cpp b/vpr/src/place/directed_moves_util.cpp deleted file mode 100644 index f1dbcea4906..00000000000 --- a/vpr/src/place/directed_moves_util.cpp +++ /dev/null @@ -1,12 +0,0 @@ - -#include "directed_moves_util.h" - -static std::map available_reward_function = { - {"basic", e_reward_function::BASIC}, - {"nonPenalizing_basic", e_reward_function::NON_PENALIZING_BASIC}, - {"runtime_aware", e_reward_function::RUNTIME_AWARE}, - {"WLbiased_runtime_aware", e_reward_function::WL_BIASED_RUNTIME_AWARE}}; - -e_reward_function string_to_reward(const std::string& st) { - return available_reward_function[st]; -} diff --git a/vpr/src/place/directed_moves_util.h b/vpr/src/place/directed_moves_util.h deleted file mode 100644 index c00666458f0..00000000000 --- a/vpr/src/place/directed_moves_util.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef VPR_DIRECTED_MOVES_UTIL_H -#define VPR_DIRECTED_MOVES_UTIL_H - -#include "globals.h" -#include "timing_place.h" - -/** - * @brief enum represents the different reward functions - */ -enum class e_reward_function { - BASIC, ///@ directly uses the change of the annealing cost function - NON_PENALIZING_BASIC, ///@ same as basic reward function but with 0 reward if it's a hill-climbing one - RUNTIME_AWARE, ///@ same as NON_PENALIZING_BASIC but with normalizing with the runtime factor of each move type - WL_BIASED_RUNTIME_AWARE, ///@ same as RUNTIME_AWARE but more biased to WL cost (the factor of the bias is REWARD_BB_TIMING_RELATIVE_WEIGHT) - UNDEFINED_REWARD ///@ Used for manual moves -}; - -e_reward_function string_to_reward(const std::string& st); - -#endif diff --git a/vpr/src/place/initial_placement.cpp b/vpr/src/place/initial_placement.cpp index f8e40afa895..e5b68674309 100644 --- a/vpr/src/place/initial_placement.cpp +++ b/vpr/src/place/initial_placement.cpp @@ -12,13 +12,9 @@ #include "place_constraints.h" #include "move_utils.h" #include "region.h" -#include "directed_moves_util.h" #include "echo_files.h" - -#include #include -#include #ifdef VERBOSE void print_clb_placement(const char* fname); diff --git a/vpr/src/place/move_generator.cpp b/vpr/src/place/move_generator.cpp index 7c7e252a050..c2920e69835 100644 --- a/vpr/src/place/move_generator.cpp +++ b/vpr/src/place/move_generator.cpp @@ -103,3 +103,13 @@ void MoveTypeStat::print_placement_move_types_stats() { } VTR_LOG("\n"); } + +static std::map available_reward_function = { + {"basic", e_reward_function::BASIC}, + {"nonPenalizing_basic", e_reward_function::NON_PENALIZING_BASIC}, + {"runtime_aware", e_reward_function::RUNTIME_AWARE}, + {"WLbiased_runtime_aware", e_reward_function::WL_BIASED_RUNTIME_AWARE}}; + +e_reward_function string_to_reward(const std::string& st) { + return available_reward_function[st]; +} diff --git a/vpr/src/place/move_generator.h b/vpr/src/place/move_generator.h index d14c14312d0..0c83bb9d5eb 100644 --- a/vpr/src/place/move_generator.h +++ b/vpr/src/place/move_generator.h @@ -4,7 +4,6 @@ #include "vpr_types.h" #include "move_utils.h" #include "timing_place.h" -#include "directed_moves_util.h" #include @@ -41,6 +40,19 @@ struct MoveTypeStat { void print_placement_move_types_stats(); }; +/** + * @brief enum represents the different reward functions + */ +enum class e_reward_function { + BASIC, ///@ directly uses the change of the annealing cost function + NON_PENALIZING_BASIC, ///@ same as basic reward function but with 0 reward if it's a hill-climbing one + RUNTIME_AWARE, ///@ same as NON_PENALIZING_BASIC but with normalizing with the runtime factor of each move type + WL_BIASED_RUNTIME_AWARE, ///@ same as RUNTIME_AWARE but more biased to WL cost (the factor of the bias is REWARD_BB_TIMING_RELATIVE_WEIGHT) + UNDEFINED_REWARD ///@ Used for manual moves +}; + +e_reward_function string_to_reward(const std::string& st); + /** * @brief a base class for move generators * diff --git a/vpr/src/place/simpleRL_move_generator.h b/vpr/src/place/simpleRL_move_generator.h index 1f33684d2f8..dea1ee67669 100644 --- a/vpr/src/place/simpleRL_move_generator.h +++ b/vpr/src/place/simpleRL_move_generator.h @@ -29,7 +29,7 @@ class KArmedBanditAgent { * @brief Update the agent Q-table based on the reward received by the SA algorithm * * @param reward A double value calculated in "place.cpp" file showing how placement cost was affected by the prior action taken - * @param reward_func The reward function used by the agent, detail explanation can be found on "directed_moves_util.h" file + * @param reward_func The reward function used by the agent, detail explanation can be found on "m.h" file */ void process_outcome(double, e_reward_function); diff --git a/vpr/src/place/weighted_median_move_generator.cpp b/vpr/src/place/weighted_median_move_generator.cpp index b887e12b54e..f3216415d80 100644 --- a/vpr/src/place/weighted_median_move_generator.cpp +++ b/vpr/src/place/weighted_median_move_generator.cpp @@ -157,7 +157,6 @@ bool WeightedMedianMoveGenerator::get_bb_cost_for_net_excluding_block(ClusterNet const auto& blk_loc_registry = placer_state_.get().blk_loc_registry(); const auto& block_locs = blk_loc_registry.block_locs(); const auto& cluster_ctx = g_vpr_ctx.clustering(); - const auto& device_ctx = g_vpr_ctx.device(); bool skip_net = true; From 13ed5c2ab531afd012c58734ac0451cf58f8c3dd Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Wed, 2 Oct 2024 13:17:53 -0400 Subject: [PATCH 017/162] don't clip bounding box coordinates between 1 and H/W -2 in MedianMoveGenerator --- vpr/src/base/place_and_route.cpp | 2 +- vpr/src/place/median_move_generator.cpp | 130 ++++++++++-------------- vpr/src/place/median_move_generator.h | 4 +- 3 files changed, 57 insertions(+), 79 deletions(-) diff --git a/vpr/src/base/place_and_route.cpp b/vpr/src/base/place_and_route.cpp index eb64a4b1959..ab5cf31ca4f 100644 --- a/vpr/src/base/place_and_route.cpp +++ b/vpr/src/base/place_and_route.cpp @@ -453,7 +453,7 @@ t_chan_width init_chan(int cfactor, const t_chan_width_dist& chan_width_dist, t_ VTR_ASSERT(num_channels > 0); float separation = 1.0 / num_channels; /* Norm. distance between two channels. */ - for (size_t i = 0; i < grid.width(); ++i) { //-2 for no perim channels + for (size_t i = 0; i < grid.width(); ++i) { float x = float(i) / num_channels; chan_width.y_list[i] = compute_chan_width(cfactor, chan_y_dist, x, separation, graph_directionality); chan_width.y_list[i] = std::max(chan_width.y_list[i], 1); //Minimum channel width 1 diff --git a/vpr/src/place/median_move_generator.cpp b/vpr/src/place/median_move_generator.cpp index 44e1732e6d2..27982315c00 100644 --- a/vpr/src/place/median_move_generator.cpp +++ b/vpr/src/place/median_move_generator.cpp @@ -98,10 +98,6 @@ e_create_move MedianMoveGenerator::propose_move(t_pl_blocks_to_be_moved& blocks_ int yold = block_loc.y + block_physical_type->pin_height_offset[pnum]; int layer_old = block_loc.layer; - xold = std::max(std::min(xold, (int)device_ctx.grid.width() - 2), 1); //-2 for no perim channels - yold = std::max(std::min(yold, (int)device_ctx.grid.height() - 2), 1); //-2 for no perim channels - layer_old = std::max(std::min(layer_old, (int)device_ctx.grid.get_num_layers() - 1), 0); - //To calculate the bb incrementally while excluding the moving block //assume that the moving block is moved to a non-critical coord of the bb int xnew; @@ -189,100 +185,91 @@ e_create_move MedianMoveGenerator::propose_move(t_pl_blocks_to_be_moved& blocks_ void MedianMoveGenerator::get_bb_from_scratch_excluding_block(ClusterNetId net_id, t_bb& bb_coord_new, - ClusterBlockId block_id, + ClusterBlockId moving_block_id, bool& skip_net) { //TODO: account for multiple physical pin instances per logical pin - const auto& placer_state = placer_state_.get(); - const auto& block_locs = placer_state.block_locs(); + const auto& blk_loc_registry = placer_state_.get().blk_loc_registry(); + const auto& cluster_ctx = g_vpr_ctx.clustering(); + /* If the net is only connected to the moving block, it should be skipped. + * Let's initially assume that the net is only connected to the moving block. + * When going through the driver and sink blocks, we check if they are the same + * as the moving block. If not, we set this flag to false. */ skip_net = true; int xmin = OPEN; int xmax = OPEN; int ymin = OPEN; int ymax = OPEN; - int layer_min = OPEN; int layer_max = OPEN; - int pnum; - - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& device_ctx = g_vpr_ctx.device(); - - ClusterBlockId bnum = cluster_ctx.clb_nlist.net_driver_block(net_id); + ClusterBlockId driver_block_id = cluster_ctx.clb_nlist.net_driver_block(net_id); bool first_block = false; - if (bnum != block_id) { + if (driver_block_id != moving_block_id) { skip_net = false; - pnum = placer_state.blk_loc_registry().net_pin_to_tile_pin_index(net_id, 0); - const t_pl_loc& block_loc = block_locs[bnum].loc; - int src_x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int src_y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - int src_layer = block_loc.layer; - - xmin = src_x; - ymin = src_y; - xmax = src_x; - ymax = src_y; - layer_min = src_layer; - layer_max = src_layer; + + // get the source pin's location + ClusterPinId source_pin_id = cluster_ctx.clb_nlist.net_pin(net_id, 0); + t_physical_tile_loc source_pin_loc = blk_loc_registry.get_coordinate_of_pin(source_pin_id); + + xmin = source_pin_loc.x; + ymin = source_pin_loc.y; + xmax = source_pin_loc.x; + ymax = source_pin_loc.y; + layer_min = source_pin_loc.layer_num; + layer_max = source_pin_loc.layer_num; first_block = true; } for (ClusterPinId pin_id : cluster_ctx.clb_nlist.net_sinks(net_id)) { - bnum = cluster_ctx.clb_nlist.pin_block(pin_id); - pnum = placer_state.blk_loc_registry().tile_pin_index(pin_id); - if (bnum == block_id) + ClusterBlockId sink_block_id = cluster_ctx.clb_nlist.pin_block(pin_id); + + if (sink_block_id == moving_block_id) { continue; + } + skip_net = false; - const auto& block_loc = block_locs[bnum].loc; - int x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - int layer = block_loc.layer; + + t_physical_tile_loc pin_loc = blk_loc_registry.get_coordinate_of_pin(pin_id); if (!first_block) { - xmin = x; - ymin = y; - xmax = x; - ymax = y; - layer_max = layer; - layer_min = layer; + xmin = pin_loc.x; + ymin = pin_loc.y; + xmax = pin_loc.x; + ymax = pin_loc.y; + layer_max = pin_loc.layer_num; + layer_min = pin_loc.layer_num; first_block = true; continue; } - if (x < xmin) { - xmin = x; - } else if (x > xmax) { - xmax = x; + + if (pin_loc.x < xmin) { + xmin = pin_loc.x; + } else if (pin_loc.x > xmax) { + xmax = pin_loc.x; } - if (y < ymin) { - ymin = y; - } else if (y > ymax) { - ymax = y; + if (pin_loc.y < ymin) { + ymin = pin_loc.y; + } else if (pin_loc.y > ymax) { + ymax = pin_loc.y; } - if (layer < layer_min) { - layer_min = layer; - } else if (layer > layer_max) { - layer_max = layer; + if (pin_loc.layer_num < layer_min) { + layer_min = pin_loc.layer_num; + } else if (pin_loc.layer_num > layer_max) { + layer_max = pin_loc.layer_num; } } - /* Now I've found the coordinates of the bounding box. There are no * - * channels beyond device_ctx.grid.width()-2 and * - * device_ctx.grid.height() - 2, so I want to clip to that. As well,* - * since I'll always include the channel immediately below and the * - * channel immediately to the left of the bounding box, I want to * - * clip to 1 in both directions as well (since minimum channel index * - * is 0). See route_common.cpp for a channel diagram. */ - bb_coord_new.xmin = std::max(std::min(xmin, device_ctx.grid.width() - 2), 1); //-2 for no perim channels - bb_coord_new.ymin = std::max(std::min(ymin, device_ctx.grid.height() - 2), 1); //-2 for no perim channels - bb_coord_new.layer_min = std::max(std::min(layer_min, device_ctx.grid.get_num_layers() - 1), 0); - bb_coord_new.xmax = std::max(std::min(xmax, device_ctx.grid.width() - 2), 1); //-2 for no perim channels - bb_coord_new.ymax = std::max(std::min(ymax, device_ctx.grid.height() - 2), 1); //-2 for no perim channels - bb_coord_new.layer_max = std::max(std::min(layer_max, device_ctx.grid.get_num_layers() - 1), 0); + bb_coord_new.xmin = xmin; + bb_coord_new.ymin = ymin; + bb_coord_new.layer_min = layer_min; + bb_coord_new.xmax = xmax; + bb_coord_new.ymax = ymax; + bb_coord_new.layer_max = layer_max; } bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, @@ -294,17 +281,8 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, int ynew, int layer_new) { //TODO: account for multiple physical pin instances per logical pin - - auto& device_ctx = g_vpr_ctx.device(); - auto& place_move_ctx = placer_state_.get().move(); - - xnew = std::max(std::min(xnew, device_ctx.grid.width() - 2), 1); //-2 for no perim channels - ynew = std::max(std::min(ynew, device_ctx.grid.height() - 2), 1); //-2 for no perim channels - layer_new = std::max(std::min(layer_new, device_ctx.grid.get_num_layers() - 1), 0); - - xold = std::max(std::min(xold, device_ctx.grid.width() - 2), 1); //-2 for no perim channels - yold = std::max(std::min(yold, device_ctx.grid.height() - 2), 1); //-2 for no perim channels - layer_old = std::max(std::min(layer_old, device_ctx.grid.get_num_layers() - 1), 0); + const auto& device_ctx = g_vpr_ctx.device(); + const auto& place_move_ctx = placer_state_.get().move(); t_bb union_bb_edge; t_bb union_bb; diff --git a/vpr/src/place/median_move_generator.h b/vpr/src/place/median_move_generator.h index 15bda1dae33..845b4c07b2c 100644 --- a/vpr/src/place/median_move_generator.h +++ b/vpr/src/place/median_move_generator.h @@ -48,7 +48,7 @@ class MedianMoveGenerator : public MoveGenerator { /** * @brief Finds the bounding box of a net and stores its coordinates in the bb_coord_new data structure. * - * @details It excludes the moving block sent in function arguments in block_id. + * @details It excludes the moving block sent in function arguments in moving_block_id. * It also returns whether this net should be excluded from median calculation or not. * This routine should only be called for small nets, since it does not determine * enough information for the bounding box to be updated incrementally later. @@ -57,7 +57,7 @@ class MedianMoveGenerator : public MoveGenerator { */ void get_bb_from_scratch_excluding_block(ClusterNetId net_id, t_bb& bb_coord_new, - ClusterBlockId block_id, + ClusterBlockId moving_block_id, bool& skip_net); }; From 359b2e193123fc3d333c84d6385b00e4719fc26b Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Wed, 2 Oct 2024 13:35:12 -0400 Subject: [PATCH 018/162] call get_coordinate_of_pin() instead of computing pin location in MedianMoveGenerator --- vpr/src/place/median_move_generator.cpp | 126 +++++++++++------------- vpr/src/place/median_move_generator.h | 4 +- 2 files changed, 58 insertions(+), 72 deletions(-) diff --git a/vpr/src/place/median_move_generator.cpp b/vpr/src/place/median_move_generator.cpp index 27982315c00..eb067d9eea8 100644 --- a/vpr/src/place/median_move_generator.cpp +++ b/vpr/src/place/median_move_generator.cpp @@ -88,49 +88,39 @@ e_create_move MedianMoveGenerator::propose_move(t_pl_blocks_to_be_moved& blocks_ } const auto& net_bb_coords = cube_bb ? place_move_ctx.bb_coords[net_id] : union_bb; - //use the incremental update of the bb - ClusterBlockId bnum = cluster_ctx.clb_nlist.pin_block(pin_id); - int pnum = blk_loc_registry.tile_pin_index(pin_id); - VTR_ASSERT(pnum >= 0); - t_pl_loc block_loc = block_locs[bnum].loc; - t_physical_tile_type_ptr block_physical_type = physical_tile_type(block_loc); - int xold = block_loc.x + block_physical_type->pin_width_offset[pnum]; - int yold = block_loc.y + block_physical_type->pin_height_offset[pnum]; - int layer_old = block_loc.layer; + t_physical_tile_loc old_pin_loc = blk_loc_registry.get_coordinate_of_pin(pin_id); + t_physical_tile_loc new_pin_loc; //To calculate the bb incrementally while excluding the moving block //assume that the moving block is moved to a non-critical coord of the bb - int xnew; - if (net_bb_coords.xmin == xold) { - xnew = net_bb_coords.xmax; + if (net_bb_coords.xmin == old_pin_loc.x) { + new_pin_loc.x = net_bb_coords.xmax; } else { - xnew = net_bb_coords.xmin; + new_pin_loc.x = net_bb_coords.xmin; } - int ynew; - if (net_bb_coords.ymin == yold) { - ynew = net_bb_coords.ymax; + if (net_bb_coords.ymin == old_pin_loc.y) { + new_pin_loc.y = net_bb_coords.ymax; } else { - ynew = net_bb_coords.ymin; + new_pin_loc.y = net_bb_coords.ymin; } - int layer_new; - if (net_bb_coords.layer_min == layer_old) { - layer_new = net_bb_coords.layer_max; + if (net_bb_coords.layer_min == old_pin_loc.layer_num) { + new_pin_loc.layer_num = net_bb_coords.layer_max; } else { - layer_new = net_bb_coords.layer_min; + new_pin_loc.layer_num = net_bb_coords.layer_min; } // If the moving block is on the border of the bounding box, we cannot get // the bounding box incrementally. In that case, bounding box should be calculated // from scratch. - if (!get_bb_incrementally(net_id, coords, xold, yold, layer_old, xnew, ynew, layer_new)) { + if (!get_bb_incrementally(net_id, coords, old_pin_loc, new_pin_loc)) { get_bb_from_scratch_excluding_block(net_id, coords, b_from, skip_net); if (skip_net) continue; } } - //push the calculated coorinates into X,Y coord vectors + //push the calculated coordinates into X,Y coord vectors place_move_ctx.X_coord.push_back(coords.xmin); place_move_ctx.X_coord.push_back(coords.xmax); place_move_ctx.Y_coord.push_back(coords.ymin); @@ -274,12 +264,8 @@ void MedianMoveGenerator::get_bb_from_scratch_excluding_block(ClusterNetId net_i bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, t_bb& bb_coord_new, - int xold, - int yold, - int layer_old, - int xnew, - int ynew, - int layer_new) { + t_physical_tile_loc old_pin_loc, + t_physical_tile_loc new_pin_loc) { //TODO: account for multiple physical pin instances per logical pin const auto& device_ctx = g_vpr_ctx.device(); const auto& place_move_ctx = placer_state_.get().move(); @@ -287,11 +273,11 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, t_bb union_bb_edge; t_bb union_bb; const bool cube_bb = g_vpr_ctx.placement().cube_bb; - /* Calculating per-layer bounding box is more time consuming compared to cube bounding box. To speed up + /* Calculating per-layer bounding box is more time-consuming compared to cube bounding box. To speed up * this move, the bounding box used for this move is of the type cube bounding box even if the per-layer * bounding box is used by placement SA engine. - * If per-layer bounding box is used, we take a union of boundinx boxes on each layer to make a cube bounding box. - * For example, the xmax of this cube boundix box is determined by the maximim x coordinate across all blocks on all layers. + * If per-layer bounding box is used, we take a union of bounding boxes on each layer to make a cube bounding box. + * For example, the xmax of this cube bounding box is determined by the maximum x coordinate across all blocks on all layers. */ if (!cube_bb) { std::tie(union_bb_edge, union_bb) = union_2d_bb_incr(place_move_ctx.layer_bb_num_on_edges[net_id], @@ -306,11 +292,11 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, /* Check if I can update the bounding box incrementally. */ - if (xnew < xold) { /* Move to left. */ + if (new_pin_loc.x < old_pin_loc.x) { /* Move to left. */ /* Update the xmax fields for coordinates and number of edges first. */ - if (xold == curr_bb_coord.xmax) { /* Old position at xmax. */ + if (old_pin_loc.x == curr_bb_coord.xmax) { /* Old position at xmax. */ if (curr_bb_edge.xmax == 1) { return false; } else { @@ -322,20 +308,20 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, /* Now do the xmin fields for coordinates and number of edges. */ - if (xnew < curr_bb_coord.xmin) { /* Moved past xmin */ - bb_coord_new.xmin = xnew; - } else if (xnew == curr_bb_coord.xmin) { /* Moved to xmin */ - bb_coord_new.xmin = xnew; + if (new_pin_loc.x < curr_bb_coord.xmin) { /* Moved past xmin */ + bb_coord_new.xmin = new_pin_loc.x; + } else if (new_pin_loc.x == curr_bb_coord.xmin) { /* Moved to xmin */ + bb_coord_new.xmin = new_pin_loc.x; } else { /* Xmin unchanged. */ bb_coord_new.xmin = curr_bb_coord.xmin; } /* End of move to left case. */ - } else if (xnew > xold) { /* Move to right. */ + } else if (new_pin_loc.x > old_pin_loc.x) { /* Move to right. */ /* Update the xmin fields for coordinates and number of edges first. */ - if (xold == curr_bb_coord.xmin) { /* Old position at xmin. */ + if (old_pin_loc.x == curr_bb_coord.xmin) { /* Old position at xmin. */ if (curr_bb_edge.xmin == 1) { return false; } else { @@ -346,27 +332,27 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, } /* Now do the xmax fields for coordinates and number of edges. */ - if (xnew > curr_bb_coord.xmax) { /* Moved past xmax. */ - bb_coord_new.xmax = xnew; - } else if (xnew == curr_bb_coord.xmax) { /* Moved to xmax */ - bb_coord_new.xmax = xnew; + if (new_pin_loc.x > curr_bb_coord.xmax) { /* Moved past xmax. */ + bb_coord_new.xmax = new_pin_loc.x; + } else if (new_pin_loc.x == curr_bb_coord.xmax) { /* Moved to xmax */ + bb_coord_new.xmax = new_pin_loc.x; } else { /* Xmax unchanged. */ bb_coord_new.xmax = curr_bb_coord.xmax; } /* End of move to right case. */ - } else { /* xnew == xold -- no x motion. */ + } else { /* new_pin_loc.x == old_pin_loc.x -- no x motion. */ bb_coord_new.xmin = curr_bb_coord.xmin; bb_coord_new.xmax = curr_bb_coord.xmax; } /* Now account for the y-direction motion. */ - if (ynew < yold) { /* Move down. */ + if (new_pin_loc.y < old_pin_loc.y) { /* Move down. */ /* Update the ymax fields for coordinates and number of edges first. */ - if (yold == curr_bb_coord.ymax) { /* Old position at ymax. */ + if (old_pin_loc.y == curr_bb_coord.ymax) { /* Old position at ymax. */ if (curr_bb_edge.ymax == 1) { return false; } else { @@ -378,20 +364,20 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, /* Now do the ymin fields for coordinates and number of edges. */ - if (ynew < curr_bb_coord.ymin) { /* Moved past ymin */ - bb_coord_new.ymin = ynew; - } else if (ynew == curr_bb_coord.ymin) { /* Moved to ymin */ - bb_coord_new.ymin = ynew; + if (new_pin_loc.y < curr_bb_coord.ymin) { /* Moved past ymin */ + bb_coord_new.ymin = new_pin_loc.y; + } else if (new_pin_loc.y == curr_bb_coord.ymin) { /* Moved to ymin */ + bb_coord_new.ymin = new_pin_loc.y; } else { /* ymin unchanged. */ bb_coord_new.ymin = curr_bb_coord.ymin; } /* End of move down case. */ - } else if (ynew > yold) { /* Moved up. */ + } else if (new_pin_loc.y > old_pin_loc.y) { /* Moved up. */ /* Update the ymin fields for coordinates and number of edges first. */ - if (yold == curr_bb_coord.ymin) { /* Old position at ymin. */ + if (old_pin_loc.y == curr_bb_coord.ymin) { /* Old position at ymin. */ if (curr_bb_edge.ymin == 1) { return false; } else { @@ -403,22 +389,22 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, /* Now do the ymax fields for coordinates and number of edges. */ - if (ynew > curr_bb_coord.ymax) { /* Moved past ymax. */ - bb_coord_new.ymax = ynew; - } else if (ynew == curr_bb_coord.ymax) { /* Moved to ymax */ - bb_coord_new.ymax = ynew; + if (new_pin_loc.y > curr_bb_coord.ymax) { /* Moved past ymax. */ + bb_coord_new.ymax = new_pin_loc.y; + } else if (new_pin_loc.y == curr_bb_coord.ymax) { /* Moved to ymax */ + bb_coord_new.ymax = new_pin_loc.y; } else { /* ymax unchanged. */ bb_coord_new.ymax = curr_bb_coord.ymax; } /* End of move up case. */ - } else { /* ynew == yold -- no y motion. */ + } else { /* new_pin_loc.y == old_pin_loc.y -- no y motion. */ bb_coord_new.ymin = curr_bb_coord.ymin; bb_coord_new.ymax = curr_bb_coord.ymax; } - if (layer_new < layer_old) { - if (layer_old == curr_bb_coord.layer_max) { + if (new_pin_loc.layer_num < old_pin_loc.layer_num) { + if (old_pin_loc.layer_num == curr_bb_coord.layer_max) { if (curr_bb_edge.layer_max == 1) { return false; } else { @@ -428,16 +414,16 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, bb_coord_new.layer_max = curr_bb_coord.layer_max; } - if (layer_new < curr_bb_coord.layer_min) { - bb_coord_new.layer_min = layer_new; - } else if (layer_new == curr_bb_coord.layer_min) { - bb_coord_new.layer_min = layer_new; + if (new_pin_loc.layer_num < curr_bb_coord.layer_min) { + bb_coord_new.layer_min = new_pin_loc.layer_num; + } else if (new_pin_loc.layer_num == curr_bb_coord.layer_min) { + bb_coord_new.layer_min = new_pin_loc.layer_num; } else { bb_coord_new.layer_min = curr_bb_coord.layer_min; } - } else if (layer_new > layer_old) { - if (layer_old == curr_bb_coord.layer_min) { + } else if (new_pin_loc.layer_num > old_pin_loc.layer_num) { + if (old_pin_loc.layer_num == curr_bb_coord.layer_min) { if (curr_bb_edge.layer_min == 1) { return false; } else { @@ -447,10 +433,10 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, bb_coord_new.layer_min = curr_bb_coord.layer_min; } - if (layer_new > curr_bb_coord.layer_max) { - bb_coord_new.layer_max = layer_new; - } else if (layer_new == curr_bb_coord.layer_max) { - bb_coord_new.layer_max = layer_new; + if (new_pin_loc.layer_num > curr_bb_coord.layer_max) { + bb_coord_new.layer_max = new_pin_loc.layer_num; + } else if (new_pin_loc.layer_num == curr_bb_coord.layer_max) { + bb_coord_new.layer_max = new_pin_loc.layer_num; } else { bb_coord_new.layer_max = curr_bb_coord.layer_max; } diff --git a/vpr/src/place/median_move_generator.h b/vpr/src/place/median_move_generator.h index 845b4c07b2c..0cafbcca2dc 100644 --- a/vpr/src/place/median_move_generator.h +++ b/vpr/src/place/median_move_generator.h @@ -41,8 +41,8 @@ class MedianMoveGenerator : public MoveGenerator { * The x and y coordinates are the pin's x and y coordinates. IO blocks are considered to be * one cell in for simplicity. */ bool get_bb_incrementally(ClusterNetId net_id, t_bb& bb_coord_new, - int xold, int yold, int layer_old, - int xnew, int ynew, int layer_new); + t_physical_tile_loc old_pin_loc, + t_physical_tile_loc new_pin_loc); /** From f822a9f89b10bd69836cfb8941165b1d5973ce14 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Wed, 2 Oct 2024 13:39:50 -0400 Subject: [PATCH 019/162] call get_coordinate_of_pin() instead of computing pin location in WeightedMedianMoveGenerator --- vpr/src/place/median_move_generator.cpp | 1 - .../place/weighted_median_move_generator.cpp | 50 ++++++++----------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/vpr/src/place/median_move_generator.cpp b/vpr/src/place/median_move_generator.cpp index eb067d9eea8..45226ec721d 100644 --- a/vpr/src/place/median_move_generator.cpp +++ b/vpr/src/place/median_move_generator.cpp @@ -267,7 +267,6 @@ bool MedianMoveGenerator::get_bb_incrementally(ClusterNetId net_id, t_physical_tile_loc old_pin_loc, t_physical_tile_loc new_pin_loc) { //TODO: account for multiple physical pin instances per logical pin - const auto& device_ctx = g_vpr_ctx.device(); const auto& place_move_ctx = placer_state_.get().move(); t_bb union_bb_edge; diff --git a/vpr/src/place/weighted_median_move_generator.cpp b/vpr/src/place/weighted_median_move_generator.cpp index f3216415d80..684dd6548ed 100644 --- a/vpr/src/place/weighted_median_move_generator.cpp +++ b/vpr/src/place/weighted_median_move_generator.cpp @@ -155,7 +155,6 @@ bool WeightedMedianMoveGenerator::get_bb_cost_for_net_excluding_block(ClusterNet const PlacerCriticalities* criticalities, t_bb_cost* coords) { const auto& blk_loc_registry = placer_state_.get().blk_loc_registry(); - const auto& block_locs = blk_loc_registry.block_locs(); const auto& cluster_ctx = g_vpr_ctx.clustering(); bool skip_net = true; @@ -176,11 +175,8 @@ bool WeightedMedianMoveGenerator::get_bb_cost_for_net_excluding_block(ClusterNet bool is_first_block = true; for (ClusterPinId pin_id : cluster_ctx.clb_nlist.net_pins(net_id)) { - ClusterBlockId bnum = cluster_ctx.clb_nlist.pin_block(pin_id); - if (pin_id != moving_pin_id) { skip_net = false; - int pnum = blk_loc_registry.tile_pin_index(pin_id); /** * Calculates the pin index of the correct pin to calculate the required connection * @@ -195,53 +191,49 @@ bool WeightedMedianMoveGenerator::get_bb_cost_for_net_excluding_block(ClusterNet } float cost = criticalities->criticality(net_id, ipin); - VTR_ASSERT(pnum >= 0); - const t_pl_loc block_loc = block_locs[bnum].loc; - int x = block_loc.x + physical_tile_type(block_loc)->pin_width_offset[pnum]; - int y = block_loc.y + physical_tile_type(block_loc)->pin_height_offset[pnum]; - int layer = block_loc.layer; + t_physical_tile_loc pin_loc = blk_loc_registry.get_coordinate_of_pin(pin_id); if (is_first_block) { - xmin = x; + xmin = pin_loc.x; xmin_cost = cost; - ymin = y; + ymin = pin_loc.y; ymin_cost = cost; - xmax = x; + xmax = pin_loc.x; xmax_cost = cost; - ymax = y; + ymax = pin_loc.y; ymax_cost = cost; - layer_min = layer; + layer_min = pin_loc.layer_num; layer_min_cost = cost; - layer_max = layer; + layer_max = pin_loc.layer_num; layer_max_cost = cost; is_first_block = false; } else { - if (x < xmin) { - xmin = x; + if (pin_loc.x < xmin) { + xmin = pin_loc.x; xmin_cost = cost; - } else if (x > xmax) { - xmax = x; + } else if (pin_loc.x > xmax) { + xmax = pin_loc.x; xmax_cost = cost; } - if (y < ymin) { - ymin = y; + if (pin_loc.y < ymin) { + ymin = pin_loc.y; ymin_cost = cost; - } else if (y > ymax) { - ymax = y; + } else if (pin_loc.y > ymax) { + ymax = pin_loc.y; ymax_cost = cost; } - if (layer < layer_min) { - layer_min = layer; + if (pin_loc.layer_num < layer_min) { + layer_min = pin_loc.layer_num; layer_min_cost = cost; - } else if (layer > layer_max) { - layer_max = layer; + } else if (pin_loc.layer_num > layer_max) { + layer_max = pin_loc.layer_num; layer_max_cost = cost; - } else if (layer == layer_min) { + } else if (pin_loc.layer_num == layer_min) { if (cost > layer_min_cost) layer_min_cost = cost; - } else if (layer == layer_max) { + } else if (pin_loc.layer_num == layer_max) { if (cost > layer_max_cost) layer_max_cost = cost; } From 5aee7d09244193dbde856b2023d2ed321f9621df Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Fri, 11 Oct 2024 16:21:09 -0400 Subject: [PATCH 020/162] update NoC options in command_line_usage.rst --- doc/src/vpr/command_line_usage.rst | 51 +++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/doc/src/vpr/command_line_usage.rst b/doc/src/vpr/command_line_usage.rst index be0896dd7c6..1c5ac2822c7 100644 --- a/doc/src/vpr/command_line_usage.rst +++ b/doc/src/vpr/command_line_usage.rst @@ -1077,7 +1077,11 @@ The following options are only used when FPGA device and netlist contain a NoC r Controls the algorithm used by the NoC to route packets. * ``xy_routing`` Uses the direction oriented routing algorithm. This is recommended to be used with mesh NoC topologies. - * ``bfs_routing`` Uses the breadth first search algorithm. The objective is to find a route that uses a minimum number of links. This can be used with any NoC topology. + * ``bfs_routing`` Uses the breadth first search algorithm. The objective is to find a route that uses a minimum number of links. This algorithm is not guaranteed to generate deadlock-free traffic flow routes, but can be used with any NoC topology. + * ``west_first_routing`` Uses the west-first routing algorithm. This is recommended to be used with mesh NoC topologies. + * ``north_last_routing`` Uses the north-last routing algorithm. This is recommended to be used with mesh NoC topologies. + * ``negative_first_routing`` Uses the negative-first routing algorithm. This is recommended to be used with mesh NoC topologies. + * ``odd_even_routing`` Uses the odd-even routing algorithm. This is recommended to be used with mesh NoC topologies. **Default:** ``bfs_routing`` @@ -1089,28 +1093,45 @@ The following options are only used when FPGA device and netlist contain a NoC r * ``noc_placement_weighting = 1`` means noc placement is considered equal to timing and wirelength. * ``noc_placement_weighting > 1`` means the placement is increasingly dominated by NoC parameters. - **Default:** ``0.6`` + **Default:** ``5.0`` + +.. option:: --noc_aggregate_bandwidth_weighting + + Controls the importance of minimizing the NoC aggregate bandwidth. This value can be >=0, where 0 would mean the aggregate bandwidth has no relevance to placement. + Other positive numbers specify the importance of minimizing the NoC aggregate bandwidth to other NoC-related cost terms. + Weighting factors for NoC-related cost terms are normalized internally. Therefore, their absolute values are not important, and + only their relative ratios determine the importance of each cost term. + + **Default:** ``0.38`` .. option:: --noc_latency_constraints_weighting - Controls the importance of meeting all the NoC traffic flow latency constraints. + Controls the importance of meeting all the NoC traffic flow latency constraints. This value can be >=0, where 0 would mean latency constraints have no relevance to placement. + Other positive numbers specify the importance of meeting latency constraints to other NoC-related cost terms. + Weighting factors for NoC-related cost terms are normalized internally. Therefore, their absolute values are not important, and + only their relative ratios determine the importance of each cost term. - * ``latency_constraints = 0`` means the latency constraints have no relevance to placement. - * ``0 < latency_constraints < 1`` means the latency constraints are weighted equally to the sum of other placement cost components. - * ``latency_constraints > 1`` means the placement is increasingly dominated by reducing the latency constraints of the traffic flows. - - **Default:** ``1`` + **Default:** ``0.6`` .. option:: --noc_latency_weighting Controls the importance of reducing the latencies of the NoC traffic flows. - This value can be >=0, + This value can be >=0, where 0 would mean the latencies have no relevance to placement + Other positive numbers specify the importance of minimizing aggregate latency to other NoC-related cost terms. + Weighting factors for NoC-related cost terms are normalized internally. Therefore, their absolute values are not important, and + only their relative ratios determine the importance of each cost term. - * ``latency = 0`` means the latencies have no relevance to placement. - * ``0 < latency < 1`` means the latencies are weighted equally to the sum of other placement cost components. - * ``latency > 1`` means the placement is increasingly dominated by reducing the latencies of the traffic flows. - - **Default:** ``0.05`` + **Default:** ``0.02`` + +.. option:: --noc_congestion_weighting + + Controls the importance of reducing the congestion of the NoC links. + This value can be >=0, where 0 would mean the congestion has no relevance to placement. + Other positive numbers specify the importance of minimizing congestion to other NoC-related cost terms. + Weighting factors for NoC-related cost terms are normalized internally. Therefore, their absolute values are not important, and + only their relative ratios determine the importance of each cost term. + + **Default:** ``0.25`` .. option:: --noc_swap_percentage @@ -1120,7 +1141,7 @@ The following options are only used when FPGA device and netlist contain a NoC r * ``0`` means NoC blocks will be moved at the same rate as other blocks. * ``100`` means all swaps attempted by the placer are NoC router blocks. - **Default:** ``40`` + **Default:** ``0`` .. option:: --noc_placement_file_name From a7d660e3aa6ffa1357f41d16cca45813c2d0823c Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Fri, 11 Oct 2024 16:29:05 -0400 Subject: [PATCH 021/162] mention all available options for --noc_routing_algorithm --- doc/src/vpr/command_line_usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/vpr/command_line_usage.rst b/doc/src/vpr/command_line_usage.rst index 1c5ac2822c7..499db2aeed0 100644 --- a/doc/src/vpr/command_line_usage.rst +++ b/doc/src/vpr/command_line_usage.rst @@ -1072,7 +1072,7 @@ The following options are only used when FPGA device and netlist contain a NoC r .. note:: noc_flows_file are required to specify if NoC optimization is turned on (--noc on). -.. option:: --noc_routing_algorithm {xy_routing | bfs_routing} +.. option:: --noc_routing_algorithm {xy_routing | bfs_routing | west_first_routing | north_last_routing | negative_first_routing | odd_even_routing} Controls the algorithm used by the NoC to route packets. From 13f04409e07deff5ddcb06b901c853abd0f09d21 Mon Sep 17 00:00:00 2001 From: ZohairZaidi Date: Fri, 11 Oct 2024 20:58:11 -0400 Subject: [PATCH 022/162] updated manual section --- doc/src/quickstart/index.rst | 69 +++++++++++++----------------------- 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/doc/src/quickstart/index.rst b/doc/src/quickstart/index.rst index 7ac82662597..4379745abd2 100644 --- a/doc/src/quickstart/index.rst +++ b/doc/src/quickstart/index.rst @@ -239,52 +239,32 @@ Let's start by making a fresh directory for us to work in: Next we need to run the three main sets of tools: -* :ref:`odin_ii` performs 'synthesis' which converts our behavioural Verilog (``.v`` file) into a circuit netlist (``.blif`` file) consisting of logic equations and FPGA architecture primitives (Flip-Flops, adders etc.), +* :ref:`Parmys` performs 'synthesis' which converts our behavioural Verilog (``.v`` file) into a circuit netlist (``.blif`` file) consisting of logic equations and FPGA architecture primitives (Flip-Flops, adders etc.), * :ref:`ABC` performs 'logic optimization' which simplifies the circuit logic, and 'technology mapping' which converts logic equations into the Look-Up-Tables (LUTs) available on an FPGA, and * :ref:`VPR` which performs packing, placement and routing of the circuit to implement it on the targeted FPGA architecture. -.. _synthesizing_with_odin_ii: -Synthesizing with ODIN II +.. _synthesizing_with_parmys: +Synthesizing with Parmys ~~~~~~~~~~~~~~~~~~~~~~~~~ -First we'll run ODIN II on our Verilog file to synthesize it into a circuit netlist, providing the options: +To synthesize our Verilog file into a circuit netlist, we will utilize the `run_vtr_flow.py` script, which streamlines the process. This command synthesizes the provided Verilog file (`blink.v`) while targeting the specified FPGA architecture (`EArch.xml`). - * ``-a $VTR_ROOT/vtr_flow/arch/timing/EArch.xml`` which specifies what FPGA architecture we are targeting, - * ``-V $VTR_ROOT/doc/src/quickstart/blink.v`` which specifies the verilog file we want to synthesize, and - * ``-o blink.odin.blif`` which specifies the name of the generated ``.blif`` circuit netlist. - -The resulting command is: +The command is as follows: .. code-block:: bash - > $VTR_ROOT/odin_ii/odin_ii \ - -a $VTR_ROOT/vtr_flow/arch/timing/EArch.xml \ - -V $VTR_ROOT/doc/src/quickstart/blink.v \ - -o blink.odin.blif - -which when run should end with something like:: - - Total time: 14.7ms - Odin ran with exit status: 0 - Odin II took 0.01 seconds (max_rss 5.1 MiB) - -where ``Odin ran with exit status: 0`` indicates Odin successfully synthesized our verilog. + > $VTR_ROOT/vtr_flow/scripts/run_vtr_flow.py \ + $VTR_ROOT/doc/src/quickstart/blink.v \ + $VTR_ROOT/vtr_flow/arch/timing/EArch.xml \ + -start parmys -end parmys -We can now take a look at the circuit which ODIN produced (``blink.odin.blif``). -The file is long and likely harder to follow than our code in ``blink.v``; however it implements the same functionality. -Some interesting highlights are shown below: +When executed, the output should indicate successful synthesis, similar to: -.. literalinclude:: blink.odin.blif - :lines: 14,40 - :caption: Instantiations of rising-edge triggered Latches (i.e. Flip-Flops) in ``blink.odin.blif`` (implements part of ``r_counter`` in blink.v) + EArch/blink OK (took 0.16 seconds, overall memory peak 20.00 MiB consumed by parmys run) -.. literalinclude:: blink.odin.blif - :lines: 17-19,21-22 - :caption: Adder primitive instantiations in ``blink.odin.blif``, used to perform addition (implements part of the ``+`` operator in blink.v) +This output confirms that the synthesis was successful and provides information on the duration and memory usage during the process. -.. literalinclude:: blink.odin.blif - :lines: 45-50 - :caption: Logic equation (.names truth-table) in ``blink.odin.blif``, implementing logical OR (implements part of the ``<`` operator in blink.v) +We can now take a look at the circuit which Parmys produced (``blink.parmys.blif``). .. seealso:: For more information on the BLIF file format see :ref:`blif_format`. @@ -297,9 +277,9 @@ Next, we'll optimize and technology map our circuit using ABC, providing the opt We'll use the following, simple ABC commands:: - read blink.odin.blif; #Read the circuit synthesized by ODIN + read blink.parmys.blif; #Read the circuit synthesized by Parmys if -K 6; #Technology map to 6 input LUTs (6-LUTs) - write_hie blink.odin.blif blink.abc_no_clock.blif #Write new circuit to blink.abc_no_clock.blif + write_hie blink.parmys.blif blink.abc_no_clock.blif #Write new circuit to blink.abc_no_clock.blif .. note:: Usually you should use a more complicated script (such as that used by :ref:`run_vtr_flow`) to ensure ABC optitmizes your circuit well. @@ -308,17 +288,18 @@ The corresponding command to run is: .. code-block:: bash > $VTR_ROOT/abc/abc \ - -c 'read blink.odin.blif; if -K 6; write_hie blink.odin.blif blink.abc_no_clock.blif' + -c 'read ~/vtr_work/quickstart/blink_manual/temp/blink.parmys.blif; if -K 6; write_hie ~/vtr_work/quickstart/blink_manual/temp/blink.parmys.blif ~/vtr_work/quickstart/blink_manual/temp/blink.abc_no_clock.blif' + When run, ABC's output should look similar to:: - ABC command line: "read blink.odin.blif; if -K 6; write_hie blink.odin.blif blink.abc_no_clock.blif". + ABC command line: "read blink.parmys.blif; if -K 6; write_hie blink.parmys.blif blink.abc_no_clock.blif". Hierarchy reader converted 6 instances of blackboxes. The network was strashed and balanced before FPGA mapping. Hierarchy writer reintroduced 6 instances of blackboxes. -If we now inspect the produced BLIF file (``blink.abc_no_clock.blif``) we see that ABC was able to significantly simplify and optimize the circuit's logic (compared to ``blink.odin.blif``): +If we now inspect the produced BLIF file (``blink.abc_no_clock.blif``) we see that ABC was able to significantly simplify and optimize the circuit's logic (compared to ``blink.parmys.blif``): .. literalinclude:: blink.abc_no_clock.blif :linenos: @@ -332,14 +313,14 @@ However, there is an issue with the above BLIF produced by ABC: the latches (ris Re-inserting clocks ^^^^^^^^^^^^^^^^^^^ -We will restore the clock information by running a script which will transfer that information from the original ODIN BLIF file (writing it to the new file ``blink.pre-vpr.blif``): +We will restore the clock information by running a script which will transfer that information from the original Parmys BLIF file (writing it to the new file ``blink.pre-vpr.blif``): .. code-block:: bash > $VTR_ROOT/vtr_flow/scripts/restore_multiclock_latch.pl \ - blink.odin.blif \ - blink.abc_no_clock.blif \ - blink.pre-vpr.blif + ~/vtr_work/quickstart/blink_manual/temp/blink.parmys.blif \ + ~/vtr_work/quickstart/blink_manual/temp/blink.abc_no_clock.blif \ + ~/vtr_work/quickstart/blink_manual/temp/blink.pre-vpr.blif If we inspect ``blink.pre-vpr.blif`` we now see that the clock (``blink^clk``) has been restored to the Flip-Flops: @@ -370,7 +351,7 @@ The resulting command is: > $VTR_ROOT/vpr/vpr \ $VTR_ROOT/vtr_flow/arch/timing/EArch.xml \ - blink --circuit_file blink.pre-vpr.blif \ + ~/vtr_work/quickstart/blink_manual/temp/blink.pre-vpr.blif \ --route_chan_width 100 and after VPR finishes we should see the resulting implementation files: @@ -387,7 +368,7 @@ We can then view the implementation as usual by appending ``--analysis --disp on > $VTR_ROOT/vpr/vpr \ $VTR_ROOT/vtr_flow/arch/timing/EArch.xml \ - blink --circuit_file blink.pre-vpr.blif \ + ~/vtr_work/quickstart/blink_manual/temp/blink.pre-vpr.blif \ --route_chan_width 100 \ --analysis --disp on From 419d8b69c6583d4a272734ea16c1983856230566 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Sat, 12 Oct 2024 13:45:44 -0400 Subject: [PATCH 023/162] avoid a copy in initial placement --- libs/libvtrutil/src/vtr_range.h | 2 +- vpr/src/place/initial_placement.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/libvtrutil/src/vtr_range.h b/libs/libvtrutil/src/vtr_range.h index 42375b24771..9674f5e45c0 100644 --- a/libs/libvtrutil/src/vtr_range.h +++ b/libs/libvtrutil/src/vtr_range.h @@ -55,7 +55,7 @@ class Range { ///@brief Return true if empty constexpr bool empty() { return begin_ == end_; } ///@brief Return the range size - constexpr size_t size() { return std::distance(begin_, end_); } + constexpr size_t size() const { return std::distance(begin_, end_); } private: T begin_; diff --git a/vpr/src/place/initial_placement.cpp b/vpr/src/place/initial_placement.cpp index 3d87c9c820e..bb76d0d6cc8 100644 --- a/vpr/src/place/initial_placement.cpp +++ b/vpr/src/place/initial_placement.cpp @@ -990,7 +990,7 @@ static void place_all_blocks(const t_placer_opts& placer_opts, const auto& device_ctx = g_vpr_ctx.device(); const auto& place_macros = blk_loc_registry.place_macros(); - auto blocks = cluster_ctx.clb_nlist.blocks(); + const auto& blocks = cluster_ctx.clb_nlist.blocks(); int number_of_unplaced_blks_in_curr_itr; @@ -1129,7 +1129,7 @@ static void alloc_and_load_movable_blocks(const vtr::vector_map Date: Sun, 13 Oct 2024 17:52:11 -0400 Subject: [PATCH 024/162] use std::vector for arch.Switches --- libs/libarchfpga/src/arch_util.cpp | 7 +- libs/libarchfpga/src/echo_arch.cpp | 42 +-- libs/libarchfpga/src/parse_switchblocks.cpp | 34 ++- libs/libarchfpga/src/parse_switchblocks.h | 2 +- libs/libarchfpga/src/physical_types.h | 4 +- .../src/read_fpga_interchange_arch.cpp | 6 +- libs/libarchfpga/src/read_xml_arch_file.cpp | 253 +++++++----------- vpr/src/base/CheckArch.cpp | 23 +- vpr/src/base/SetupVPR.cpp | 20 +- 9 files changed, 164 insertions(+), 227 deletions(-) diff --git a/libs/libarchfpga/src/arch_util.cpp b/libs/libarchfpga/src/arch_util.cpp index 437f879c0bb..d95379db32d 100644 --- a/libs/libarchfpga/src/arch_util.cpp +++ b/libs/libarchfpga/src/arch_util.cpp @@ -152,11 +152,10 @@ void free_arch(t_arch* arch) { return; } - delete[] arch->Switches; - arch->Switches = nullptr; - free_arch_models(arch->models); + vtr::release_memory(arch->switches); + vtr::release_memory(arch->Directs); vtr::free(arch->architecture_id); @@ -230,7 +229,7 @@ t_model* free_arch_model(t_model* model) { return next_model; } -//Frees all the model portss in a linked list +//Frees all the model ports in a linked list void free_arch_model_ports(t_model_ports* model_ports) { t_model_ports* model_port = model_ports; while (model_port) { diff --git a/libs/libarchfpga/src/echo_arch.cpp b/libs/libarchfpga/src/echo_arch.cpp index 5b9e89f5ee3..54525fc53b7 100644 --- a/libs/libarchfpga/src/echo_arch.cpp +++ b/libs/libarchfpga/src/echo_arch.cpp @@ -253,29 +253,29 @@ void PrintArchInfo(FILE* Echo, const t_arch* arch) { //13 is hard coded because format of %e is always 1.123456e+12 //It always consists of 10 alphanumeric digits, a decimal //and a sign - for (i = 0; i < arch->num_switches; i++) { - if (arch->Switches[i].type() == SwitchType::MUX) { - fprintf(Echo, "\tSwitch[%d]: name %s type mux\n", i + 1, arch->Switches[i].name.c_str()); - } else if (arch->Switches[i].type() == SwitchType::TRISTATE) { - fprintf(Echo, "\tSwitch[%d]: name %s type tristate\n", i + 1, arch->Switches[i].name.c_str()); - } else if (arch->Switches[i].type() == SwitchType::SHORT) { - fprintf(Echo, "\tSwitch[%d]: name %s type short\n", i + 1, arch->Switches[i].name.c_str()); - } else if (arch->Switches[i].type() == SwitchType::BUFFER) { - fprintf(Echo, "\tSwitch[%d]: name %s type buffer\n", i + 1, arch->Switches[i].name.c_str()); + for (i = 0; i < (int)arch->switches.size(); i++) { + if (arch->switches[i].type() == SwitchType::MUX) { + fprintf(Echo, "\tSwitch[%d]: name %s type mux\n", i + 1, arch->switches[i].name.c_str()); + } else if (arch->switches[i].type() == SwitchType::TRISTATE) { + fprintf(Echo, "\tSwitch[%d]: name %s type tristate\n", i + 1, arch->switches[i].name.c_str()); + } else if (arch->switches[i].type() == SwitchType::SHORT) { + fprintf(Echo, "\tSwitch[%d]: name %s type short\n", i + 1, arch->switches[i].name.c_str()); + } else if (arch->switches[i].type() == SwitchType::BUFFER) { + fprintf(Echo, "\tSwitch[%d]: name %s type buffer\n", i + 1, arch->switches[i].name.c_str()); } else { - VTR_ASSERT(arch->Switches[i].type() == SwitchType::PASS_GATE); - fprintf(Echo, "\tSwitch[%d]: name %s type pass_gate\n", i + 1, arch->Switches[i].name.c_str()); + VTR_ASSERT(arch->switches[i].type() == SwitchType::PASS_GATE); + fprintf(Echo, "\tSwitch[%d]: name %s type pass_gate\n", i + 1, arch->switches[i].name.c_str()); } - fprintf(Echo, "\t\t\t\tR %e Cin %e Cout %e\n", arch->Switches[i].R, - arch->Switches[i].Cin, arch->Switches[i].Cout); + fprintf(Echo, "\t\t\t\tR %e Cin %e Cout %e\n", arch->switches[i].R, + arch->switches[i].Cin, arch->switches[i].Cout); fprintf(Echo, "\t\t\t\t#Tdel values %d buf_size %e mux_trans_size %e\n", - (int)arch->Switches[i].Tdel_map_.size(), arch->Switches[i].buf_size, - arch->Switches[i].mux_trans_size); - if (arch->Switches[i].power_buffer_type == POWER_BUFFER_TYPE_AUTO) { + (int)arch->switches[i].Tdel_map_.size(), arch->switches[i].buf_size, + arch->switches[i].mux_trans_size); + if (arch->switches[i].power_buffer_type == POWER_BUFFER_TYPE_AUTO) { fprintf(Echo, "\t\t\t\tpower_buffer_size auto\n"); } else { fprintf(Echo, "\t\t\t\tpower_buffer_size %e\n", - arch->Switches[i].power_buffer_size); + arch->switches[i].power_buffer_size); } } @@ -293,19 +293,19 @@ void PrintArchInfo(FILE* Echo, const t_arch* arch) { if (seg.directionality == UNI_DIRECTIONAL) { //wire_switch == arch_opin_switch fprintf(Echo, "\t\t\t\ttype unidir mux_name for within die connections: %s\n", - arch->Switches[seg.arch_wire_switch].name.c_str()); + arch->switches[seg.arch_wire_switch].name.c_str()); //if there is more than one layer available, print the segment switch name that is used for connection between two dice for (const auto& layout : arch->grid_layouts) { int num_layers = (int)layout.layers.size(); if (num_layers > 1) { fprintf(Echo, "\t\t\t\ttype unidir mux_name for between two dice connections: %s\n", - arch->Switches[seg.arch_opin_between_dice_switch].name.c_str()); + arch->switches[seg.arch_opin_between_dice_switch].name.c_str()); } } } else { //Should be bidir fprintf(Echo, "\t\t\t\ttype bidir wire_switch %s arch_opin_switch %s\n", - arch->Switches[seg.arch_wire_switch].name.c_str(), - arch->Switches[seg.arch_opin_switch].name.c_str()); + arch->switches[seg.arch_wire_switch].name.c_str(), + arch->switches[seg.arch_opin_switch].name.c_str()); } fprintf(Echo, "\t\t\t\tcb "); diff --git a/libs/libarchfpga/src/parse_switchblocks.cpp b/libs/libarchfpga/src/parse_switchblocks.cpp index c0b55bcb7fd..1e8908713bf 100644 --- a/libs/libarchfpga/src/parse_switchblocks.cpp +++ b/libs/libarchfpga/src/parse_switchblocks.cpp @@ -41,16 +41,16 @@ using vtr::t_formula_data; /*---- Functions for Parsing Switchblocks from Architecture ----*/ //Load an XML wireconn specification into a t_wireconn_inf -t_wireconn_inf parse_wireconn(pugi::xml_node node, const pugiutil::loc_data& loc_data, const t_arch_switch_inf* switches, int num_switches); +static t_wireconn_inf parse_wireconn(pugi::xml_node node, const pugiutil::loc_data& loc_data, const std::vector& switches); //Process the desired order of a wireconn static void parse_switchpoint_order(const char* order, SwitchPointOrder& switchpoint_order); //Process a wireconn defined in the inline style (using attributes) -void parse_wireconn_inline(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const t_arch_switch_inf* switches, int num_switches); +static void parse_wireconn_inline(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const std::vector& switches); //Process a wireconn defined in the multinode style (more advanced specification) -void parse_wireconn_multinode(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const t_arch_switch_inf* switches, int num_switches); +static void parse_wireconn_multinode(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const std::vector& switches); //Process a or sub-node of a multinode wireconn t_wire_switchpoints parse_wireconn_from_to_node(pugi::xml_node node, const pugiutil::loc_data& loc_data); @@ -69,7 +69,7 @@ static void parse_num_conns(std::string num_conns, t_wireconn_inf& wireconn); static void set_switch_func_type(SB_Side_Connection& conn, const char* func_type); /* parse switch_override in wireconn */ -static void parse_switch_override(const char* switch_override, t_wireconn_inf& wireconn, const t_arch_switch_inf* switches, int num_switches); +static void parse_switch_override(const char* switch_override, t_wireconn_inf& wireconn, const std::vector& switches); /* checks for correctness of a unidir switchblock. */ static void check_unidir_switchblock(const t_switchblock_inf* sb); @@ -85,7 +85,7 @@ static void check_wireconn(const t_arch* arch, const t_wireconn_inf& wireconn); /*---- Functions for Parsing Switchblocks from Architecture ----*/ /* Reads-in the wire connections specified for the switchblock in the xml arch file */ -void read_sb_wireconns(const t_arch_switch_inf* switches, int num_switches, pugi::xml_node Node, t_switchblock_inf* sb, const pugiutil::loc_data& loc_data) { +void read_sb_wireconns(const std::vector& switches, pugi::xml_node Node, t_switchblock_inf* sb, const pugiutil::loc_data& loc_data) { /* Make sure that Node is a switchblock */ check_node(Node, "switchblock", loc_data); @@ -100,31 +100,29 @@ void read_sb_wireconns(const t_arch_switch_inf* switches, int num_switches, pugi SubElem = get_first_child(Node, "wireconn", loc_data); } for (int i = 0; i < num_wireconns; i++) { - t_wireconn_inf wc = parse_wireconn(SubElem, loc_data, switches, num_switches); // need to pass in switch info for switch override + t_wireconn_inf wc = parse_wireconn(SubElem, loc_data, switches); // need to pass in switch info for switch override sb->wireconns.push_back(wc); SubElem = SubElem.next_sibling(SubElem.name()); } - - return; } -t_wireconn_inf parse_wireconn(pugi::xml_node node, const pugiutil::loc_data& loc_data, const t_arch_switch_inf* switches, int num_switches) { +static t_wireconn_inf parse_wireconn(pugi::xml_node node, const pugiutil::loc_data& loc_data, const std::vector& switches) { t_wireconn_inf wc; size_t num_children = count_children(node, "from", loc_data, ReqOpt::OPTIONAL); num_children += count_children(node, "to", loc_data, ReqOpt::OPTIONAL); if (num_children == 0) { - parse_wireconn_inline(node, loc_data, wc, switches, num_switches); + parse_wireconn_inline(node, loc_data, wc, switches); } else { VTR_ASSERT(num_children > 0); - parse_wireconn_multinode(node, loc_data, wc, switches, num_switches); + parse_wireconn_multinode(node, loc_data, wc, switches); } return wc; } -void parse_wireconn_inline(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const t_arch_switch_inf* switches, int num_switches) { +static void parse_wireconn_inline(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const std::vector& switches) { //Parse an inline wireconn definition, using attributes expect_only_attributes(node, {"num_conns", "from_type", "to_type", "from_switchpoint", "to_switchpoint", "from_order", "to_order", "switch_override"}, loc_data); @@ -156,10 +154,10 @@ void parse_wireconn_inline(pugi::xml_node node, const pugiutil::loc_data& loc_da // parse switch overrides if they exist: char_prop = get_attribute(node, "switch_override", loc_data, ReqOpt::OPTIONAL).value(); - parse_switch_override(char_prop, wc, switches, num_switches); + parse_switch_override(char_prop, wc, switches); } -void parse_wireconn_multinode(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const t_arch_switch_inf* switches, int num_switches) { +void parse_wireconn_multinode(pugi::xml_node node, const pugiutil::loc_data& loc_data, t_wireconn_inf& wc, const std::vector& switches) { expect_only_children(node, {"from", "to"}, loc_data); /* get the connection style */ @@ -173,7 +171,7 @@ void parse_wireconn_multinode(pugi::xml_node node, const pugiutil::loc_data& loc parse_switchpoint_order(char_prop, wc.to_switchpoint_order); char_prop = get_attribute(node, "switch_override", loc_data, ReqOpt::OPTIONAL).value(); - parse_switch_override(char_prop, wc, switches, num_switches); + parse_switch_override(char_prop, wc, switches); size_t num_from_children = count_children(node, "from", loc_data); size_t num_to_children = count_children(node, "to", loc_data); @@ -378,11 +376,9 @@ void read_sb_switchfuncs(pugi::xml_node Node, t_switchblock_inf* sb, const pugiu /* get the next switchblock function */ SubElem = SubElem.next_sibling(SubElem.name()); } - - return; } -static void parse_switch_override(const char* switch_override, t_wireconn_inf& wireconn, const t_arch_switch_inf* switches, int num_switches) { +static void parse_switch_override(const char* switch_override, t_wireconn_inf& wireconn, const std::vector& switches) { // sentinel value to use default driving switch for the receiving wire type if (switch_override == std::string("")) { wireconn.switch_override_indx = DEFAULT_SWITCH; //Default @@ -390,7 +386,7 @@ static void parse_switch_override(const char* switch_override, t_wireconn_inf& w } // iterate through the valid switch names in the arch looking for the requested switch_override - for (int i = 0; i < num_switches; i++) { + for (int i = 0; i < (int)switches.size(); i++) { if (0 == strcmp(switch_override, switches[i].name.c_str())) { wireconn.switch_override_indx = i; return; diff --git a/libs/libarchfpga/src/parse_switchblocks.h b/libs/libarchfpga/src/parse_switchblocks.h index a7686031818..93777f965b6 100644 --- a/libs/libarchfpga/src/parse_switchblocks.h +++ b/libs/libarchfpga/src/parse_switchblocks.h @@ -11,7 +11,7 @@ void read_sb_switchfuncs(pugi::xml_node Node, t_switchblock_inf* sb, const pugiutil::loc_data& loc_data); /* Reads-in the wire connections specified for the switchblock in the xml arch file */ -void read_sb_wireconns(const t_arch_switch_inf* switches, int num_switches, pugi::xml_node Node, t_switchblock_inf* sb, const pugiutil::loc_data& loc_data); +void read_sb_wireconns(const std::vector& switches, pugi::xml_node Node, t_switchblock_inf* sb, const pugiutil::loc_data& loc_data); /* checks for correctness of switch block read-in from the XML architecture file */ void check_switchblock(const t_switchblock_inf* sb, const t_arch* arch); diff --git a/libs/libarchfpga/src/physical_types.h b/libs/libarchfpga/src/physical_types.h index 19ebb2718cb..23fb1893ec7 100644 --- a/libs/libarchfpga/src/physical_types.h +++ b/libs/libarchfpga/src/physical_types.h @@ -2059,8 +2059,8 @@ struct t_arch { int Fs; float grid_logic_tile_area; std::vector Segments; - t_arch_switch_inf* Switches = nullptr; - int num_switches; + + std::vector switches; std::vector Directs; t_model* models = nullptr; diff --git a/libs/libarchfpga/src/read_fpga_interchange_arch.cpp b/libs/libarchfpga/src/read_fpga_interchange_arch.cpp index d1062ae0480..b0da3e903d3 100644 --- a/libs/libarchfpga/src/read_fpga_interchange_arch.cpp +++ b/libs/libarchfpga/src/read_fpga_interchange_arch.cpp @@ -2374,15 +2374,13 @@ struct ArchReader { size_t num_switches = pip_timing_models.size() + 2; std::string switch_name; - arch_->num_switches = num_switches; - if (num_switches > 0) { - arch_->Switches = new t_arch_switch_inf[num_switches]; + arch_->switches.resize(num_switches); } float R, Cin, Cint, Cout, Tdel; for (size_t i = 0; i < num_switches; ++i) { - t_arch_switch_inf* as = &arch_->Switches[i]; + t_arch_switch_inf* as = &arch_->switches[i]; R = Cin = Cint = Cout = Tdel = 0.0; SwitchType type; diff --git a/libs/libarchfpga/src/read_xml_arch_file.cpp b/libs/libarchfpga/src/read_xml_arch_file.cpp index 685d444e6fa..61f0260eb63 100644 --- a/libs/libarchfpga/src/read_xml_arch_file.cpp +++ b/libs/libarchfpga/src/read_xml_arch_file.cpp @@ -318,16 +318,14 @@ static void ProcessComplexBlocks(pugi::xml_node Node, bool timing_enabled, const pugiutil::loc_data& loc_data); -static void ProcessSwitches(pugi::xml_node Node, - t_arch_switch_inf** Switches, - int* NumSwitches, - const bool timing_enabled, - const pugiutil::loc_data& loc_data); -static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, const int switch_index, t_arch_switch_inf* Switches, const pugiutil::loc_data& loc_data); +static std::vector ProcessSwitches(pugi::xml_node Node, + const bool timing_enabled, + const pugiutil::loc_data& loc_data); + +static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, t_arch_switch_inf& arch_switch, const pugiutil::loc_data& loc_data); static std::vector ProcessDirects(pugi::xml_node Parent, - const t_arch_switch_inf* Switches, - const int NumSwitches, + const std::vector& switches, const pugiutil::loc_data& loc_data); static void ProcessClockMetalLayers(pugi::xml_node parent, @@ -335,26 +333,23 @@ static void ProcessClockMetalLayers(pugi::xml_node parent, pugiutil::loc_data& loc_data); static void ProcessClockNetworks(pugi::xml_node parent, std::vector& clock_networks, - const t_arch_switch_inf* switches, - const int num_switches, + const std::vector& switches, pugiutil::loc_data& loc_data); static void ProcessClockSwitchPoints(pugi::xml_node parent, t_clock_network_arch& clock_network, - const t_arch_switch_inf* switches, - const int num_switches, + const std::vector& switches, pugiutil::loc_data& loc_data); static void ProcessClockRouting(pugi::xml_node parent, std::vector& clock_connections, - const t_arch_switch_inf* switches, - const int num_switches, + const std::vector& switches, pugiutil::loc_data& loc_data); -static void ProcessSegments(pugi::xml_node Parent, - std::vector& Segs, - const t_arch_switch_inf* Switches, - const int NumSwitches, - const bool timing_enabled, - const bool switchblocklist_required, - const pugiutil::loc_data& loc_data); + +static std::vector ProcessSegments(pugi::xml_node Parent, + const std::vector& switches, + const bool timing_enabled, + const bool switchblocklist_required, + const pugiutil::loc_data& loc_data); + static void ProcessSwitchblocks(pugi::xml_node Parent, t_arch* arch, const pugiutil::loc_data& loc_data); static void ProcessCB_SB(pugi::xml_node Node, std::vector& list, const pugiutil::loc_data& loc_data); static void ProcessPower(pugi::xml_node parent, @@ -373,7 +368,7 @@ static bool attribute_to_bool(const pugi::xml_node node, const pugi::xml_attribute attr, const pugiutil::loc_data& loc_data); -static int find_switch_by_name(const t_arch& arch, const std::string& switch_name); +static int find_switch_by_name(const std::vector& switches, const std::string& switch_name); static e_side string_to_side(const std::string& side_str); @@ -446,8 +441,7 @@ void XmlReadArch(const char* ArchFile, /* Process switches */ Next = get_single_child(architecture, "switchlist", loc_data); - ProcessSwitches(Next, &(arch->Switches), &(arch->num_switches), - timing_enabled, loc_data); + arch->switches = ProcessSwitches(Next, timing_enabled, loc_data); /* Process switchblocks. This depends on switches */ bool switchblocklist_required = (arch->SBType == CUSTOM); //require this section only if custom switchblocks are used @@ -455,8 +449,7 @@ void XmlReadArch(const char* ArchFile, /* Process segments. This depends on switches */ Next = get_single_child(architecture, "segmentlist", loc_data); - ProcessSegments(Next, arch->Segments, - arch->Switches, arch->num_switches, timing_enabled, switchblocklist_required, loc_data); + arch->Segments = ProcessSegments(Next, arch->switches, timing_enabled, switchblocklist_required, loc_data); Next = get_single_child(architecture, "switchblocklist", loc_data, SWITCHBLOCKLIST_REQD); if (Next) { @@ -477,7 +470,7 @@ void XmlReadArch(const char* ArchFile, /* Process directs */ Next = get_single_child(architecture, "directlist", loc_data, ReqOpt::OPTIONAL); if (Next) { - arch->Directs = ProcessDirects(Next, arch->Switches, arch->num_switches, loc_data); + arch->Directs = ProcessDirects(Next, arch->switches, loc_data); } /* Process Clock Networks */ @@ -487,15 +480,15 @@ void XmlReadArch(const char* ArchFile, expect_only_children(Next, expected_children, loc_data); ProcessClockMetalLayers(Next, arch->clock_arch.clock_metal_layers, loc_data); + ProcessClockNetworks(Next, arch->clock_arch.clock_networks_arch, - arch->Switches, - arch->num_switches, + arch->switches, loc_data); + ProcessClockRouting(Next, arch->clock_arch.clock_connections_arch, - arch->Switches, - arch->num_switches, + arch->switches, loc_data); } @@ -2138,7 +2131,7 @@ static void ProcessSwitchblockLocations(pugi::xml_node switchblock_locations, if (sb_switch_override_attr) { std::string sb_switch_override_str = sb_switch_override_attr.as_string(); //Use the specified switch - sb_switch_override = find_switch_by_name(arch, sb_switch_override_str); + sb_switch_override = find_switch_by_name(arch.switches, sb_switch_override_str); if (sb_switch_override == OPEN) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(switchblock_locations), @@ -2187,7 +2180,7 @@ static void ProcessSwitchblockLocations(pugi::xml_node switchblock_locations, if (internal_switch_attr) { std::string internal_switch_name = internal_switch_attr.as_string(); //Use the specified switch - internal_switch = find_switch_by_name(arch, internal_switch_name); + internal_switch = find_switch_by_name(arch.switches, internal_switch_name); if (internal_switch == OPEN) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(switchblock_locations), @@ -3637,16 +3630,15 @@ static void ProcessComplexBlocks(pugi::xml_node Node, } } -static void ProcessSegments(pugi::xml_node Parent, - std::vector& Segs, - const t_arch_switch_inf* Switches, - const int NumSwitches, - const bool timing_enabled, - const bool switchblocklist_required, - const pugiutil::loc_data& loc_data) { - int i, j, length; +static std::vector ProcessSegments(pugi::xml_node Parent, + const std::vector& switches, + const bool timing_enabled, + const bool switchblocklist_required, + const pugiutil::loc_data& loc_data) { const char* tmp; + std::vector Segs; + pugi::xml_node SubElem; pugi::xml_node Node; @@ -3665,7 +3657,7 @@ static void ProcessSegments(pugi::xml_node Parent, bool x_axis_seg_found = false; /*Flags to see if we have any x-directed segment type specified*/ bool y_axis_seg_found = false; /*Flags to see if we have any y-directed segment type specified*/ - for (i = 0; i < NumSegs; ++i) { + for (int i = 0; i < NumSegs; ++i) { /* Get segment name */ tmp = get_attribute(Node, "name", loc_data, ReqOpt::OPTIONAL).as_string(nullptr); if (tmp) { @@ -3685,7 +3677,7 @@ static void ProcessSegments(pugi::xml_node Parent, } /* Get segment length */ - length = 1; /* DEFAULT */ + int length = 1; /* DEFAULT */ tmp = get_attribute(Node, "length", loc_data, ReqOpt::OPTIONAL).as_string(nullptr); if (tmp) { if (strcmp(tmp, "longline") == 0) { @@ -3789,16 +3781,12 @@ static void ProcessSegments(pugi::xml_node Parent, tmp = get_attribute(SubElem, "name", loc_data, ReqOpt::OPTIONAL).as_string(""); if (strlen(tmp) != 0) { /* Match names */ - for (j = 0; j < NumSwitches; ++j) { - if (0 == strcmp(tmp, Switches[j].name.c_str())) { - break; /* End loop so j is where we want it */ - } - } - if (j >= NumSwitches) { + int switch_idx = find_switch_by_name(switches, tmp); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(SubElem), "'%s' is not a valid mux name.\n", tmp); } - Segs[i].arch_opin_between_dice_switch = j; + Segs[i].arch_opin_between_dice_switch = switch_idx; } /* Get the wire and opin switches, or mux switch if unidir */ @@ -3810,12 +3798,8 @@ static void ProcessSegments(pugi::xml_node Parent, //check if tag is defined in the architecture, otherwise we should look for and if(tmp){ /* Match names */ - for (j = 0; j < NumSwitches; ++j) { - if (0 == strcmp(tmp, Switches[j].name.c_str())) { - break; /* End loop so j is where we want it */ - } - } - if (j >= NumSwitches) { + int switch_idx = find_switch_by_name(switches, tmp); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(SubElem), "'%s' is not a valid mux name.\n", tmp); } @@ -3823,8 +3807,8 @@ static void ProcessSegments(pugi::xml_node Parent, /* Unidir muxes must have the same switch * for wire and opin fanin since there is * really only the mux in unidir. */ - Segs[i].arch_wire_switch = j; - Segs[i].arch_opin_switch = j; + Segs[i].arch_wire_switch = switch_idx; + Segs[i].arch_opin_switch = switch_idx; } else { //if a general mux is not defined, we should look for specific mux for each direction in the architecture file SubElem = get_single_child(Node, "mux_inc", loc_data, ReqOpt::OPTIONAL); @@ -3834,12 +3818,8 @@ static void ProcessSegments(pugi::xml_node Parent, "if mux is not specified in a wire segment, both mux_inc and mux_dec should be specified"); } else{ /* Match names */ - for (j = 0; j < NumSwitches; ++j) { - if (0 == strcmp(tmp, Switches[j].name.c_str())) { - break; /* End loop so j is where we want it */ - } - } - if (j >= NumSwitches) { + int switch_idx = find_switch_by_name(switches, tmp); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(SubElem), "'%s' is not a valid mux name.\n", tmp); } @@ -3847,8 +3827,8 @@ static void ProcessSegments(pugi::xml_node Parent, /* Unidir muxes must have the same switch * for wire and opin fanin since there is * really only the mux in unidir. */ - Segs[i].arch_wire_switch = j; - Segs[i].arch_opin_switch = j; + Segs[i].arch_wire_switch = switch_idx; + Segs[i].arch_opin_switch = switch_idx; } SubElem = get_single_child(Node, "mux_dec", loc_data, ReqOpt::OPTIONAL); @@ -3858,12 +3838,8 @@ static void ProcessSegments(pugi::xml_node Parent, "if mux is not specified in a wire segment, both mux_inc and mux_dec should be specified"); } else{ /* Match names */ - for (j = 0; j < NumSwitches; ++j) { - if (0 == strcmp(tmp, Switches[j].name.c_str())) { - break; /* End loop so j is where we want it */ - } - } - if (j >= NumSwitches) { + int switch_idx = find_switch_by_name(switches, tmp); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(SubElem), "'%s' is not a valid mux name.\n", tmp); } @@ -3871,8 +3847,8 @@ static void ProcessSegments(pugi::xml_node Parent, /* Unidir muxes must have the same switch * for wire and opin fanin since there is * really only the mux in unidir. */ - Segs[i].arch_wire_switch_dec = j; - Segs[i].arch_opin_switch_dec = j; + Segs[i].arch_wire_switch_dec = switch_idx; + Segs[i].arch_opin_switch_dec = switch_idx; } } } @@ -3882,35 +3858,27 @@ static void ProcessSegments(pugi::xml_node Parent, tmp = get_attribute(SubElem, "name", loc_data).value(); /* Match names */ - for (j = 0; j < NumSwitches; ++j) { - if (0 == strcmp(tmp, Switches[j].name.c_str())) { - break; /* End loop so j is where we want it */ - } - } - if (j >= NumSwitches) { + int switch_idx = find_switch_by_name(switches, tmp); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(SubElem), "'%s' is not a valid wire_switch name.\n", tmp); } - Segs[i].arch_wire_switch = j; + Segs[i].arch_wire_switch = switch_idx; SubElem = get_single_child(Node, "opin_switch", loc_data); tmp = get_attribute(SubElem, "name", loc_data).value(); /* Match names */ - for (j = 0; j < NumSwitches; ++j) { - if (0 == strcmp(tmp, Switches[j].name.c_str())) { - break; /* End loop so j is where we want it */ - } - } - if (j >= NumSwitches) { + switch_idx = find_switch_by_name(switches, tmp); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(SubElem), "'%s' is not a valid opin_switch name.\n", tmp); } - Segs[i].arch_opin_switch = j; + Segs[i].arch_opin_switch = switch_idx; } /* Setup the CB list if they give one, otherwise use full */ Segs[i].cb.resize(length); - for (j = 0; j < length; ++j) { + for (int j = 0; j < length; ++j) { Segs[i].cb[j] = true; } SubElem = get_single_child(Node, "cb", loc_data, ReqOpt::OPTIONAL); @@ -3920,7 +3888,7 @@ static void ProcessSegments(pugi::xml_node Parent, /* Setup the SB list if they give one, otherwise use full */ Segs[i].sb.resize(length + 1); - for (j = 0; j < (length + 1); ++j) { + for (int j = 0; j < (length + 1); ++j) { Segs[i].sb[j] = true; } SubElem = get_single_child(Node, "sb", loc_data, ReqOpt::OPTIONAL); @@ -3939,6 +3907,8 @@ static void ProcessSegments(pugi::xml_node Parent, archfpga_throw(loc_data.filename_c_str(), loc_data.line(Node), "Atleast one segment per-axis needs to get specified if no segments with non-specified (default) axis attribute exist."); } + + return Segs; } @@ -4080,7 +4050,7 @@ static void ProcessSwitchblocks(pugi::xml_node Parent, t_arch* arch, const pugiu SubElem = get_first_child(Node, "switchfuncs", loc_data); read_sb_switchfuncs(SubElem, &sb, loc_data); - read_sb_wireconns(arch->Switches, arch->num_switches, Node, &sb, loc_data); + read_sb_wireconns(arch->switches, Node, &sb, loc_data); /* run error checks on switch blocks */ check_switchblock(&sb, arch); @@ -4151,12 +4121,9 @@ static void ProcessCB_SB(pugi::xml_node Node, std::vector& list, const pug } } -static void ProcessSwitches(pugi::xml_node Parent, - t_arch_switch_inf** Switches, - int* NumSwitches, - const bool timing_enabled, - const pugiutil::loc_data& loc_data) { - int i, j; +static std::vector ProcessSwitches(pugi::xml_node Parent, + const bool timing_enabled, + const pugiutil::loc_data& loc_data) { const char* type_name; const char* switch_name; ReqOpt TIMING_ENABLE_REQD = BoolToReqOpt(timing_enabled); @@ -4164,18 +4131,18 @@ static void ProcessSwitches(pugi::xml_node Parent, pugi::xml_node Node; /* Count the children and check they are switches */ - *NumSwitches = count_children(Parent, "switch", loc_data); + int n_switches = count_children(Parent, "switch", loc_data); + std::vector switches; /* Alloc switch list */ - *Switches = nullptr; - if (*NumSwitches > 0) { - (*Switches) = new t_arch_switch_inf[(*NumSwitches)]; + if (n_switches > 0) { + switches.resize(n_switches); } /* Load the switches. */ Node = get_first_child(Parent, "switch", loc_data); - for (i = 0; i < *NumSwitches; ++i) { - t_arch_switch_inf& arch_switch = (*Switches)[i]; + for (int i = 0; i < n_switches; ++i) { + t_arch_switch_inf& arch_switch = switches[i]; switch_name = get_attribute(Node, "name", loc_data).value(); @@ -4189,8 +4156,8 @@ static void ProcessSwitches(pugi::xml_node Parent, type_name = get_attribute(Node, "type", loc_data).value(); /* Check for switch name collisions */ - for (j = 0; j < i; ++j) { - if (0 == strcmp((*Switches)[j].name.c_str(), switch_name)) { + for (int j = 0; j < i; ++j) { + if (0 == strcmp(switches[j].name.c_str(), switch_name)) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(Node), "Two switches with the same name '%s' were found.\n", switch_name); @@ -4282,12 +4249,14 @@ static void ProcessSwitches(pugi::xml_node Parent, arch_switch.intra_tile = false; } - //Load the Tdel (which may be specfied with sub-tags) - ProcessSwitchTdel(Node, timing_enabled, i, (*Switches), loc_data); + //Load the Tdel (which may be specified with sub-tags) + ProcessSwitchTdel(Node, timing_enabled, arch_switch, loc_data); /* Get next switch element */ Node = Node.next_sibling(Node.name()); } + + return switches; } /* Processes the switch delay. Switch delay can be specified in two ways. @@ -4299,20 +4268,17 @@ static void ProcessSwitches(pugi::xml_node Parent, * * are specified as children of the switch node. In this case, Tdel * is not included as a property of the switch node (first way). */ -static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, const int switch_index, t_arch_switch_inf* Switches, const pugiutil::loc_data& loc_data) { - float Tdel_prop_value; - int num_Tdel_children; - +static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, t_arch_switch_inf& arch_switch, const pugiutil::loc_data& loc_data) { /* check if switch node has the Tdel property */ bool has_Tdel_prop = false; - Tdel_prop_value = get_attribute(Node, "Tdel", loc_data, ReqOpt::OPTIONAL).as_float(UNDEFINED); + float Tdel_prop_value = get_attribute(Node, "Tdel", loc_data, ReqOpt::OPTIONAL).as_float(UNDEFINED); if (Tdel_prop_value != UNDEFINED) { has_Tdel_prop = true; } /* check if switch node has Tdel children */ bool has_Tdel_children = false; - num_Tdel_children = count_children(Node, "Tdel", loc_data, ReqOpt::OPTIONAL); + int num_Tdel_children = count_children(Node, "Tdel", loc_data, ReqOpt::OPTIONAL); if (num_Tdel_children != 0) { has_Tdel_children = true; } @@ -4326,7 +4292,7 @@ static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, co /* get pointer to the switch's Tdel map, then read-in delay data into this map */ if (has_Tdel_prop) { /* delay specified as a constant */ - Switches[switch_index].set_Tdel(t_arch_switch_inf::UNDEFINED_FANIN, Tdel_prop_value); + arch_switch.set_Tdel(t_arch_switch_inf::UNDEFINED_FANIN, Tdel_prop_value); } else if (has_Tdel_children) { /* Delay specified as a function of switch fan-in. * Go through each Tdel child, read-in num_inputs and the delay value. @@ -4341,7 +4307,7 @@ static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, co archfpga_throw(loc_data.filename_c_str(), loc_data.line(Tdel_child), "Tdel node specified num_inputs (%d) that has already been specified by another Tdel node", num_inputs); } else { - Switches[switch_index].set_Tdel(num_inputs, Tdel_value); + arch_switch.set_Tdel(num_inputs, Tdel_value); seen_fanins.insert(num_inputs); } Tdel_child = Tdel_child.next_sibling(Tdel_child.name()); @@ -4353,14 +4319,13 @@ static void ProcessSwitchTdel(pugi::xml_node Node, const bool timing_enabled, co "Switch should contain intrinsic delay information if timing is enabled"); } else { /* set a default value */ - Switches[switch_index].set_Tdel(t_arch_switch_inf::UNDEFINED_FANIN, 0.); + arch_switch.set_Tdel(t_arch_switch_inf::UNDEFINED_FANIN, 0.); } } } static std::vector ProcessDirects(pugi::xml_node Parent, - const t_arch_switch_inf* Switches, - const int NumSwitches, + const std::vector& switches, const pugiutil::loc_data& loc_data) { /* Count the children and check they are direct connections */ expect_only_children(Parent, {"direct"}, loc_data); @@ -4409,17 +4374,12 @@ static std::vector ProcessDirects(pugi::xml_node Parent, const char* switch_name = get_attribute(Node, "switch_name", loc_data, ReqOpt::OPTIONAL).as_string(nullptr); if (switch_name != nullptr) { //Look-up the user defined switch - int j; - for (j = 0; j < NumSwitches; j++) { - if (0 == strcmp(switch_name, Switches[j].name.c_str())) { - break; //Found the switch - } - } - if (j >= NumSwitches) { + int switch_idx = find_switch_by_name(switches, switch_name); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(Node), "Could not find switch named '%s' in switch list.\n", switch_name); } - directs[i].switch_type = j; //Save the correct switch index + directs[i].switch_type = switch_idx; //Save the correct switch index } else { //If not defined, use the delayless switch by default //TODO: find a better way of indicating this. Ideally, we would @@ -4473,8 +4433,7 @@ static void ProcessClockMetalLayers(pugi::xml_node parent, static void ProcessClockNetworks(pugi::xml_node parent, std::vector& clock_networks, - const t_arch_switch_inf* switches, - const int num_switches, + const std::vector& switches, pugiutil::loc_data& loc_data) { std::vector expected_spine_attributes = {"name", "num_inst", "metal_layer", "starty", "endy", "x", "repeatx", "repeaty"}; std::vector expected_rib_attributes = {"name", "num_inst", "metal_layer", "startx", "endx", "y", "repeatx", "repeaty"}; @@ -4528,7 +4487,7 @@ static void ProcessClockNetworks(pugi::xml_node parent, clock_network.repeat.x = repeatx; clock_network.repeat.y = repeaty; - ProcessClockSwitchPoints(curr_type, clock_network, switches, num_switches, loc_data); + ProcessClockSwitchPoints(curr_type, clock_network, switches, loc_data); } // Parse rib @@ -4566,7 +4525,7 @@ static void ProcessClockNetworks(pugi::xml_node parent, clock_network.repeat.x = repeatx; clock_network.repeat.y = repeaty; - ProcessClockSwitchPoints(curr_type, clock_network, switches, num_switches, loc_data); + ProcessClockSwitchPoints(curr_type, clock_network, switches, loc_data); } // Currently their is only support for ribs and spines @@ -4584,8 +4543,7 @@ static void ProcessClockNetworks(pugi::xml_node parent, static void ProcessClockSwitchPoints(pugi::xml_node parent, t_clock_network_arch& clock_network, - const t_arch_switch_inf* switches, - const int num_switches, + const std::vector& switches, pugiutil::loc_data& loc_data) { std::vector expected_spine_drive_attributes = {"name", "type", "yoffset", "switch_name"}; std::vector expected_rib_drive_attributes = {"name", "type", "xoffset", "switch_name"}; @@ -4598,7 +4556,7 @@ static void ProcessClockSwitchPoints(pugi::xml_node parent, //TODO: currently only supporting one drive and one tap. Should change to support // multiple taps - VTR_ASSERT(num_switches != 2); + VTR_ASSERT(switches.size() != 2); //TODO: ensure switch name is unique for every switch of this clock network for (int i = 0; i < num_clock_switches; i++) { @@ -4621,13 +4579,8 @@ static void ProcessClockSwitchPoints(pugi::xml_node parent, // get switch index const char* switch_name = get_attribute(curr_switch, "switch_name", loc_data).value(); - int switch_idx; - for (switch_idx = 0; switch_idx < num_switches; switch_idx++) { - if (0 == strcmp(switch_name, switches[switch_idx].name.c_str())) { - break; // switch_idx has been found - } - } - if (switch_idx >= num_switches) { + int switch_idx = find_switch_by_name(switches, switch_name); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(curr_switch), "'%s' is not a valid switch name.\n", switch_name); } @@ -4672,8 +4625,7 @@ static void ProcessClockSwitchPoints(pugi::xml_node parent, static void ProcessClockRouting(pugi::xml_node parent, std::vector& clock_connections, - const t_arch_switch_inf* switches, - const int num_switches, + const std::vector& switches, pugiutil::loc_data& loc_data) { std::vector expected_attributes = {"from", "to", "switch", "fc_val", "locationx", "locationy"}; @@ -4693,13 +4645,8 @@ static void ProcessClockRouting(pugi::xml_node parent, const char* locationy = get_attribute(curr_connection, "locationy", loc_data, ReqOpt::OPTIONAL).value(); float fc = get_attribute(curr_connection, "fc_val", loc_data).as_float(0.); - int switch_idx; - for (switch_idx = 0; switch_idx < num_switches; switch_idx++) { - if (0 == strcmp(switch_name, switches[switch_idx].name.c_str())) { - break; // switch_idx has been found - } - } - if (switch_idx >= num_switches) { + int switch_idx = find_switch_by_name(switches, switch_name); + if (switch_idx < 0) { archfpga_throw(loc_data.filename_c_str(), loc_data.line(curr_connection), "'%s' is not a valid switch name.\n", switch_name); } @@ -4831,15 +4778,15 @@ static bool attribute_to_bool(const pugi::xml_node node, return false; } -static int find_switch_by_name(const t_arch& arch, const std::string& switch_name) { - for (int iswitch = 0; iswitch < arch.num_switches; ++iswitch) { - const t_arch_switch_inf& arch_switch = arch.Switches[iswitch]; +static int find_switch_by_name(const std::vector& switches, const std::string& switch_name) { + for (int iswitch = 0; iswitch < (int)switches.size(); ++iswitch) { + const t_arch_switch_inf& arch_switch = switches[iswitch]; if (arch_switch.name == switch_name) { return iswitch; } } - return OPEN; + return -1; } static e_side string_to_side(const std::string& side_str) { diff --git a/vpr/src/base/CheckArch.cpp b/vpr/src/base/CheckArch.cpp index b8a324bb14f..98ca07a6e59 100644 --- a/vpr/src/base/CheckArch.cpp +++ b/vpr/src/base/CheckArch.cpp @@ -20,38 +20,37 @@ void CheckArch(const t_arch& Arch) { } static void CheckSwitches(const t_arch& Arch) { - t_arch_switch_inf* CurSwitch; - int i; + int ipin_cblock_switch_index = UNDEFINED; int ipin_cblock_switch_index_between_dice = UNDEFINED; /* Check transistors in switches won't be less than minimum size */ - CurSwitch = Arch.Switches; - for (i = 0; i < Arch.num_switches; i++) { + for (int i = 0; i < (int)Arch.switches.size(); i++) { + const t_arch_switch_inf& CurSwitch = Arch.switches[i]; /* This assumes all segments have the same directionality */ - if (CurSwitch->buffered() + if (CurSwitch.buffered() && Arch.Segments[0].directionality == BI_DIRECTIONAL) { /* Largest resistance tri-state buffer would have a minimum * width transistor in the buffer pull-down and a min-width * pass transistoron the output. * Hence, largest R = 2 * largest_transistor_R. */ - if (CurSwitch->R > 2 * Arch.R_minW_nmos) { + if (CurSwitch.R > 2 * Arch.R_minW_nmos) { vpr_throw(VPR_ERROR_ARCH, get_arch_file_name(), 0, "Switch %s R value (%g) is greater than 2 * R_minW_nmos (%g).\n" "Refer to switchlist section of '%s'\n", - CurSwitch->name.c_str(), CurSwitch->R, (2 * Arch.R_minW_nmos)); + CurSwitch.name.c_str(), CurSwitch.R, (2 * Arch.R_minW_nmos)); } } else { /* Pass transistor switch */ - if (CurSwitch->R > Arch.R_minW_nmos) { + if (CurSwitch.R > Arch.R_minW_nmos) { vpr_throw(VPR_ERROR_ARCH, get_arch_file_name(), 0, "Switch %s R value (%g) is greater than R_minW_nmos (%g).\n" "Refer to switchlist section of '%s'\n", - CurSwitch->name.c_str(), CurSwitch->R, Arch.R_minW_nmos, get_arch_file_name()); + CurSwitch.name.c_str(), CurSwitch.R, Arch.R_minW_nmos, get_arch_file_name()); } } for (auto cb_switch_name = 0; cb_switch_name < (int)Arch.ipin_cblock_switch_name.size(); cb_switch_name++) { /* find the ipin cblock switch index, if it exists */ - if (Arch.Switches[i].name == Arch.ipin_cblock_switch_name[cb_switch_name]) { + if (Arch.switches[i].name == Arch.ipin_cblock_switch_name[cb_switch_name]) { if (cb_switch_name == 0) { ipin_cblock_switch_index = i; } else { @@ -68,13 +67,13 @@ static void CheckSwitches(const t_arch& Arch) { * index to point to a switch with a routing resource switch with a representative Tdel value. * See rr_graph.c:alloc_and_load_rr_switch_inf for more info */ if (ipin_cblock_switch_index != UNDEFINED) { - if (!Arch.Switches[ipin_cblock_switch_index].fixed_Tdel()) { + if (!Arch.switches[ipin_cblock_switch_index].fixed_Tdel()) { VPR_FATAL_ERROR(VPR_ERROR_ARCH, "Not currently allowing an ipin cblock switch to have fanin dependent values"); } } if (ipin_cblock_switch_index_between_dice != UNDEFINED) { - if (!Arch.Switches[ipin_cblock_switch_index_between_dice].fixed_Tdel()) { + if (!Arch.switches[ipin_cblock_switch_index_between_dice].fixed_Tdel()) { VPR_FATAL_ERROR(VPR_ERROR_ARCH, "Not currently allowing an ipin cblock switch to have fanin dependent values"); } diff --git a/vpr/src/base/SetupVPR.cpp b/vpr/src/base/SetupVPR.cpp index 7e9b7177c48..d6315762786 100644 --- a/vpr/src/base/SetupVPR.cpp +++ b/vpr/src/base/SetupVPR.cpp @@ -44,8 +44,7 @@ static void SetupRoutingArch(const t_arch& Arch, t_det_routing_arch* RoutingArch static void SetupTiming(const t_options& Options, const bool TimingEnabled, t_timing_inf* Timing); static void SetupSwitches(const t_arch& Arch, t_det_routing_arch* RoutingArch, - const t_arch_switch_inf* ArchSwitches, - int NumArchSwitches); + const std::vector& arch_switches); static void SetupAnalysisOpts(const t_options& Options, t_analysis_opts& analysis_opts); static void SetupPowerOpts(const t_options& Options, t_power_opts* power_opts, t_arch* Arch); @@ -232,7 +231,7 @@ void SetupVPR(const t_options* options, segments = arch->Segments; - SetupSwitches(*arch, routingArch, arch->Switches, arch->num_switches); + SetupSwitches(*arch, routingArch, arch->switches); SetupRoutingArch(*arch, routingArch); SetupTiming(*options, timingenabled, timing); SetupPackerOpts(*options, packerOpts); @@ -363,12 +362,11 @@ static void SetupTiming(const t_options& Options, const bool TimingEnabled, t_ti */ static void SetupSwitches(const t_arch& Arch, t_det_routing_arch* RoutingArch, - const t_arch_switch_inf* ArchSwitches, - int NumArchSwitches) { + const std::vector& arch_switches) { auto& device_ctx = g_vpr_ctx.mutable_device(); - int switches_to_copy = NumArchSwitches; - int num_arch_switches = NumArchSwitches; + int switches_to_copy = (int)arch_switches.size(); + int num_arch_switches = (int)arch_switches.size();; find_ipin_cblock_switch_index(Arch, RoutingArch->wire_to_arch_ipin_switch, RoutingArch->wire_to_arch_ipin_switch_between_dice); @@ -378,10 +376,10 @@ static void SetupSwitches(const t_arch& Arch, /* Alloc the list now that we know the final num_arch_switches value */ device_ctx.arch_switch_inf.resize(num_arch_switches); for (int iswitch = 0; iswitch < switches_to_copy; iswitch++) { - device_ctx.arch_switch_inf[iswitch] = ArchSwitches[iswitch]; + device_ctx.arch_switch_inf[iswitch] = arch_switches[iswitch]; // TODO: AM: Since I am not sure whether replacing arch_switch_in with all_sw_inf, which contains the // information about intra-tile switched, would not break anything, for the time being, I decided to not remove it - device_ctx.all_sw_inf[iswitch] = ArchSwitches[iswitch]; + device_ctx.all_sw_inf[iswitch] = arch_switches[iswitch]; } /* Delayless switch for connecting sinks and sources with their pins. */ @@ -796,8 +794,8 @@ static void SetupServerOpts(const t_options& Options, t_server_opts* ServerOpts) static void find_ipin_cblock_switch_index(const t_arch& Arch, int& wire_to_arch_ipin_switch, int& wire_to_arch_ipin_switch_between_dice) { for (auto cb_switch_name_index = 0; cb_switch_name_index < (int)Arch.ipin_cblock_switch_name.size(); cb_switch_name_index++) { int ipin_cblock_switch_index = UNDEFINED; - for (int iswitch = 0; iswitch < Arch.num_switches; ++iswitch) { - if (Arch.Switches[iswitch].name == Arch.ipin_cblock_switch_name[cb_switch_name_index]) { + for (int iswitch = 0; iswitch < (int)Arch.switches.size(); ++iswitch) { + if (Arch.switches[iswitch].name == Arch.ipin_cblock_switch_name[cb_switch_name_index]) { if (ipin_cblock_switch_index != UNDEFINED) { VPR_FATAL_ERROR(VPR_ERROR_ARCH, "Found duplicate switches named '%s'\n", Arch.ipin_cblock_switch_name[cb_switch_name_index].c_str()); From 1bfe098b64c2e62eaf77a7e48f6d370a5a14f642 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Mon, 14 Oct 2024 14:46:59 -0400 Subject: [PATCH 025/162] typos and comments --- libs/libarchfpga/src/physical_types.h | 12 ++++++------ libs/libarchfpga/src/read_xml_arch_file.cpp | 16 +++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/libs/libarchfpga/src/physical_types.h b/libs/libarchfpga/src/physical_types.h index 23fb1893ec7..44adbedc95b 100644 --- a/libs/libarchfpga/src/physical_types.h +++ b/libs/libarchfpga/src/physical_types.h @@ -1380,11 +1380,11 @@ class t_pb_graph_pin { float thld = std::numeric_limits::quiet_NaN(); /* For sequential logic elements the hold time */ float tco_min = std::numeric_limits::quiet_NaN(); /* For sequential logic elements the minimum clock to output time */ float tco_max = std::numeric_limits::quiet_NaN(); /* For sequential logic elements the maximum clock to output time */ - t_pb_graph_pin* associated_clock_pin = nullptr; /* For sequentail elements, the associated clock */ + t_pb_graph_pin* associated_clock_pin = nullptr; /* For sequential elements, the associated clock */ /* This member is used when flat-routing and has_choking_spot are enabled. * It is used to identify choke points. - * This is only valid for IPINs, and it only contain the pins that are reachable to the pin by a forwarding path. + * This is only valid for IPINs, and it only contains the pins that are reachable to the pin by a forwarding path. * It doesn't take into account feed-back connection. * */ std::unordered_set connected_sinks_ptc; /* ptc numbers of sinks which are directly or indirectly connected to this pin */ @@ -1708,7 +1708,7 @@ enum class BufferSize { * parallel stream of pass transistors feeding into a buffer, * * we would expect an additional "internal capacitance" * * to arise when the pass transistor is enabled and the signal * - * must propogate to the buffer. See diagram of one stream below: * + * must propagate to the buffer. See diagram of one stream below: * * * * Pass Transistor * * | * @@ -1823,7 +1823,7 @@ struct t_rr_switch_inf { SwitchType type() const; //Returns true if this switch type isolates its input and output into - //seperate DC-connected subcircuits + //separate DC-connected subcircuits bool buffered() const; //Returns true if this switch type is configurable @@ -1894,7 +1894,7 @@ struct t_wireconn_inf { * elements (if 'from' is larger than 'to'), or in some elements of 'to' having * no driving connections (if 'to' is larger than 'from'). * 'to': The number of generated connections is set equal to the size of the 'to' set. - * This ensures that each element of the 'to' set has precisely one incomming connection. + * This ensures that each element of the 'to' set has precisely one incoming connection. * Note: this may result in 'from' elements driving multiple 'to' elements (if 'to' is * larger than 'from'), or some 'from' elements driving to 'to' elements (if 'from' is * larger than 'to') @@ -1948,7 +1948,7 @@ struct t_switchblock_inf { e_directionality directionality; /* the directionality of this switchblock (unidir/bidir) */ int x = -1; /* The exact x-axis location that this SB is used, meaningful when type is set to E_XY_specified */ - int y = -1; /* The exact y-axis location that this SB is used, meanignful when type is set to E_XY_specified */ + int y = -1; /* The exact y-axis location that this SB is used, meaningful when type is set to E_XY_specified */ /* We can also define a region to apply this SB to all locations falls into this region using regular expression in the architecture file*/ t_sb_loc_spec reg_x; diff --git a/libs/libarchfpga/src/read_xml_arch_file.cpp b/libs/libarchfpga/src/read_xml_arch_file.cpp index 61f0260eb63..15a3b51091d 100644 --- a/libs/libarchfpga/src/read_xml_arch_file.cpp +++ b/libs/libarchfpga/src/read_xml_arch_file.cpp @@ -368,7 +368,14 @@ static bool attribute_to_bool(const pugi::xml_node node, const pugi::xml_attribute attr, const pugiutil::loc_data& loc_data); -static int find_switch_by_name(const std::vector& switches, const std::string& switch_name); +/** + * @brief Searches for a switch whose matches with the given name. + * @param switches Contains all the architecture switches. + * @param switch_name The name with which switch names are compared. + * @return A negative integer if no switch was found with the given name; otherwise + * the index of the matching switch is returned. + */ +static int find_switch_by_name(const std::vector& switches, std::string_view switch_name); static e_side string_to_side(const std::string& side_str); @@ -4722,10 +4729,9 @@ static void ProcessPower(pugi::xml_node parent, } } -/* Get the clock architcture */ +/* Get the clock architecture */ static void ProcessClocks(pugi::xml_node Parent, t_clock_arch* clocks, const pugiutil::loc_data& loc_data) { pugi::xml_node Node; - int i; const char* tmp; clocks->num_global_clocks = count_children(Parent, "clock", loc_data, ReqOpt::OPTIONAL); @@ -4740,7 +4746,7 @@ static void ProcessClocks(pugi::xml_node Parent, t_clock_arch* clocks, const pug /* Load the clock info. */ Node = get_first_child(Parent, "clock", loc_data); - for (i = 0; i < clocks->num_global_clocks; ++i) { + for (int i = 0; i < clocks->num_global_clocks; ++i) { tmp = get_attribute(Node, "buffer_size", loc_data).value(); if (strcmp(tmp, "auto") == 0) { clocks->clock_inf[i].autosize_buffer = true; @@ -4778,7 +4784,7 @@ static bool attribute_to_bool(const pugi::xml_node node, return false; } -static int find_switch_by_name(const std::vector& switches, const std::string& switch_name) { +static int find_switch_by_name(const std::vector& switches, std::string_view switch_name) { for (int iswitch = 0; iswitch < (int)switches.size(); ++iswitch) { const t_arch_switch_inf& arch_switch = switches[iswitch]; if (arch_switch.name == switch_name) { From ea74465eb7f83796496952b0a4ca7e19005cf10e Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Mon, 14 Oct 2024 14:59:12 -0400 Subject: [PATCH 026/162] non-static member variable in CentroidMoveGenerator --- vpr/src/place/centroid_move_generator.cpp | 21 +++------------------ vpr/src/place/centroid_move_generator.h | 14 +++++++------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/vpr/src/place/centroid_move_generator.cpp b/vpr/src/place/centroid_move_generator.cpp index a4b39fc1be9..1e40bf227f5 100644 --- a/vpr/src/place/centroid_move_generator.cpp +++ b/vpr/src/place/centroid_move_generator.cpp @@ -7,14 +7,6 @@ #include - -// Static member variable definitions -vtr::vector> CentroidMoveGenerator::noc_group_clusters_; -vtr::vector> CentroidMoveGenerator::noc_group_routers_; -vtr::vector CentroidMoveGenerator::cluster_to_noc_grp_; -std::map CentroidMoveGenerator::noc_router_to_noc_group_; - - CentroidMoveGenerator::CentroidMoveGenerator(PlacerState& placer_state, e_reward_function reward_function) : MoveGenerator(placer_state, reward_function) @@ -31,14 +23,7 @@ CentroidMoveGenerator::CentroidMoveGenerator(PlacerState& placer_state, , noc_attraction_enabled_(true) { VTR_ASSERT(noc_attraction_weight > 0.0 && noc_attraction_weight <= 1.0); - - // check if static member variables are already initialized - if (!noc_group_clusters_.empty() && !noc_group_routers_.empty() && - !cluster_to_noc_grp_.empty() && !noc_router_to_noc_group_.empty()) { - return; - } else { - initialize_noc_groups(high_fanout_net); - } + initialize_noc_groups(high_fanout_net); } e_create_move CentroidMoveGenerator::propose_move(t_pl_blocks_to_be_moved& blocks_affected, @@ -105,11 +90,11 @@ e_create_move CentroidMoveGenerator::propose_move(t_pl_blocks_to_be_moved& block } const std::vector& CentroidMoveGenerator::get_noc_group_routers(NocGroupId noc_grp_id) { - return CentroidMoveGenerator::noc_group_routers_[noc_grp_id]; + return noc_group_routers_[noc_grp_id]; } NocGroupId CentroidMoveGenerator::get_cluster_noc_group(ClusterBlockId blk_id) { - return CentroidMoveGenerator::cluster_to_noc_grp_[blk_id]; + return cluster_to_noc_grp_[blk_id]; } void CentroidMoveGenerator::initialize_noc_groups(size_t high_fanout_net) { diff --git a/vpr/src/place/centroid_move_generator.h b/vpr/src/place/centroid_move_generator.h index 1a4af272e00..f9a9c6772b6 100644 --- a/vpr/src/place/centroid_move_generator.h +++ b/vpr/src/place/centroid_move_generator.h @@ -58,7 +58,7 @@ class CentroidMoveGenerator : public MoveGenerator { * @param noc_grp_id The NoC group ID whose NoC routers are requested. * @return The clustered block ID of all NoC routers in the given NoC group. */ - static const std::vector& get_noc_group_routers(NocGroupId noc_grp_id); + const std::vector& get_noc_group_routers(NocGroupId noc_grp_id); /** * Returns the NoC group ID of clustered block. @@ -66,7 +66,7 @@ class CentroidMoveGenerator : public MoveGenerator { * @return The NoC group ID of the given clustered block or INVALID if * the given clustered block does not belong to any NoC groups. */ - static NocGroupId get_cluster_noc_group(ClusterBlockId blk_id); + NocGroupId get_cluster_noc_group(ClusterBlockId blk_id); private: e_create_move propose_move(t_pl_blocks_to_be_moved& blocks_affected, @@ -112,20 +112,20 @@ class CentroidMoveGenerator : public MoveGenerator { bool noc_attraction_enabled_; /** Stores the ids of all non-router clustered blocks for each NoC group*/ - static vtr::vector> noc_group_clusters_; + vtr::vector> noc_group_clusters_; /** Stores NoC routers in each NoC group*/ - static vtr::vector> noc_group_routers_; + vtr::vector> noc_group_routers_; /** Specifies the NoC group that each block belongs to. A block cannot belong to more * than one NoC because this means those NoC groups can reach each other and form * a single NoC group. We use NocGroupId::INVALID to show that a block does not belong * to any NoC groups. This happens when a block is not reachable from any NoC router. * */ - static vtr::vector cluster_to_noc_grp_; + vtr::vector cluster_to_noc_grp_; /** Specifies the NoC group for each NoC router*/ - static std::map noc_router_to_noc_group_; + std::map noc_router_to_noc_group_; /** * @brief This function forms NoC groups by finding connected components @@ -135,7 +135,7 @@ class CentroidMoveGenerator : public MoveGenerator { * @param high_fanout_net All nets with a fanout larger than this number are * ignored when forming NoC groups. */ - static void initialize_noc_groups(size_t high_fanout_net); + void initialize_noc_groups(size_t high_fanout_net); }; #endif From b41cda3eb66219006e0ef8cbf3087237a23bb941 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 15 Oct 2024 14:46:53 -0400 Subject: [PATCH 027/162] add annealer.cpp/.h --- vpr/src/base/ShowSetup.cpp | 10 +-- vpr/src/base/read_options.cpp | 6 +- vpr/src/base/read_options.h | 2 +- vpr/src/base/vpr_types.h | 4 +- vpr/src/place/annealer.cpp | 145 ++++++++++++++++++++++++++++++++++ vpr/src/place/annealer.h | 135 +++++++++++++++++++++++++++++++ vpr/src/place/place.cpp | 6 +- vpr/src/place/place_util.cpp | 140 +------------------------------- vpr/src/place/place_util.h | 124 ----------------------------- 9 files changed, 295 insertions(+), 277 deletions(-) create mode 100644 vpr/src/place/annealer.cpp create mode 100644 vpr/src/place/annealer.h diff --git a/vpr/src/base/ShowSetup.cpp b/vpr/src/base/ShowSetup.cpp index f288be8f865..68aa073759d 100644 --- a/vpr/src/base/ShowSetup.cpp +++ b/vpr/src/base/ShowSetup.cpp @@ -207,13 +207,13 @@ void writeClusteredNetlistStats(const std::string& block_usage_filename) { static void ShowAnnealSched(const t_annealing_sched& AnnealSched) { VTR_LOG("AnnealSched.type: "); switch (AnnealSched.type) { - case AUTO_SCHED: + case e_sched_type::AUTO_SCHED: VTR_LOG("AUTO_SCHED\n"); break; - case USER_SCHED: + case e_sched_type::USER_SCHED: VTR_LOG("USER_SCHED\n"); break; - case DUSTY_SCHED: + case e_sched_type::DUSTY_SCHED: VTR_LOG("DUSTY_SCHED\n"); break; default: @@ -222,11 +222,11 @@ static void ShowAnnealSched(const t_annealing_sched& AnnealSched) { VTR_LOG("AnnealSched.inner_num: %f\n", AnnealSched.inner_num); - if (USER_SCHED == AnnealSched.type) { + if (e_sched_type::USER_SCHED == AnnealSched.type) { VTR_LOG("AnnealSched.init_t: %f\n", AnnealSched.init_t); VTR_LOG("AnnealSched.alpha_t: %f\n", AnnealSched.alpha_t); VTR_LOG("AnnealSched.exit_t: %f\n", AnnealSched.exit_t); - } else if (DUSTY_SCHED == AnnealSched.type) { + } else if (e_sched_type::DUSTY_SCHED == AnnealSched.type) { VTR_LOG("AnnealSched.alpha_min: %f\n", AnnealSched.alpha_min); VTR_LOG("AnnealSched.alpha_max: %f\n", AnnealSched.alpha_max); VTR_LOG("AnnealSched.alpha_decay: %f\n", AnnealSched.alpha_decay); diff --git a/vpr/src/base/read_options.cpp b/vpr/src/base/read_options.cpp index fa7084a9b07..eeb4bbfaee0 100644 --- a/vpr/src/base/read_options.cpp +++ b/vpr/src/base/read_options.cpp @@ -3141,13 +3141,13 @@ void set_conditional_defaults(t_options& args) { || args.PlaceAlphaDecay.provenance() == Provenance::SPECIFIED || args.PlaceSuccessMin.provenance() == Provenance::SPECIFIED || args.PlaceSuccessTarget.provenance() == Provenance::SPECIFIED) { - args.anneal_sched_type.set(DUSTY_SCHED, Provenance::INFERRED); + args.anneal_sched_type.set(e_sched_type::DUSTY_SCHED, Provenance::INFERRED); } else if (args.PlaceInitT.provenance() == Provenance::SPECIFIED // Any of these flags select a manual schedule || args.PlaceExitT.provenance() == Provenance::SPECIFIED || args.PlaceAlphaT.provenance() == Provenance::SPECIFIED) { - args.anneal_sched_type.set(USER_SCHED, Provenance::INFERRED); + args.anneal_sched_type.set(e_sched_type::USER_SCHED, Provenance::INFERRED); } else { - args.anneal_sched_type.set(AUTO_SCHED, Provenance::INFERRED); // Otherwise use the automatic schedule + args.anneal_sched_type.set(e_sched_type::AUTO_SCHED, Provenance::INFERRED); // Otherwise use the automatic schedule } /* diff --git a/vpr/src/base/read_options.h b/vpr/src/base/read_options.h index c07762350d5..673694fd80c 100644 --- a/vpr/src/base/read_options.h +++ b/vpr/src/base/read_options.h @@ -124,7 +124,7 @@ struct t_options { argparse::ArgValue PlaceAlphaDecay; argparse::ArgValue PlaceSuccessMin; argparse::ArgValue PlaceSuccessTarget; - argparse::ArgValue anneal_sched_type; + argparse::ArgValue anneal_sched_type; argparse::ArgValue PlaceAlgorithm; argparse::ArgValue PlaceQuenchAlgorithm; argparse::ArgValue pad_loc_type; diff --git a/vpr/src/base/vpr_types.h b/vpr/src/base/vpr_types.h index 98f17e898a1..84432ed7181 100644 --- a/vpr/src/base/vpr_types.h +++ b/vpr/src/base/vpr_types.h @@ -460,7 +460,7 @@ constexpr int NUM_PL_MOVE_TYPES = 7; constexpr int NUM_PL_NONTIMING_MOVE_TYPES = 3; /* Timing data structures end */ -enum sched_type { +enum class e_sched_type { AUTO_SCHED, DUSTY_SCHED, USER_SCHED @@ -836,7 +836,7 @@ struct t_packer_opts { * the obvious meanings. */ struct t_annealing_sched { - enum sched_type type; + e_sched_type type; float inner_num; float init_t; float alpha_t; diff --git a/vpr/src/place/annealer.cpp b/vpr/src/place/annealer.cpp new file mode 100644 index 00000000000..b820acdaf92 --- /dev/null +++ b/vpr/src/place/annealer.cpp @@ -0,0 +1,145 @@ + +#include "annealer.h" + +#include +#include + +#include "globals.h" +#include "draw_global.h" +#include "vpr_types.h" +#include "place_util.h" + +///@brief Constructor: Initialize all annealing state variables and macros. +t_annealing_state::t_annealing_state(const t_annealing_sched& annealing_sched, + float first_t, + float first_rlim, + int first_move_lim, + float first_crit_exponent) { + num_temps = 0; + alpha = annealing_sched.alpha_min; + t = first_t; + restart_t = first_t; + rlim = first_rlim; + move_lim_max = first_move_lim; + crit_exponent = first_crit_exponent; + + /* Determine the current move_lim based on the schedule type */ + if (annealing_sched.type == e_sched_type::DUSTY_SCHED) { + move_lim = std::max(1, (int)(move_lim_max * annealing_sched.success_target)); + } else { + move_lim = move_lim_max; + } + + /* Store this inverse value for speed when updating crit_exponent. */ + INVERSE_DELTA_RLIM = 1 / (first_rlim - FINAL_RLIM); + + /* The range limit cannot exceed the largest grid size. */ + const auto& grid = g_vpr_ctx.device().grid; + UPPER_RLIM = std::max(grid.width() - 1, grid.height() - 1); +} + +bool t_annealing_state::outer_loop_update(float success_rate, + const t_placer_costs& costs, + const t_placer_opts& placer_opts, + const t_annealing_sched& annealing_sched) { +#ifndef NO_GRAPHICS + t_draw_state* draw_state = get_draw_state_vars(); + if (!draw_state->list_of_breakpoints.empty()) { + /* Update temperature in the current information variable. */ + get_bp_state_globals()->get_glob_breakpoint_state()->temp_count++; + } +#endif + + if (annealing_sched.type == e_sched_type::USER_SCHED) { + /* Update t with user specified alpha. */ + t *= annealing_sched.alpha_t; + + /* Check if the exit criterion is met. */ + bool exit_anneal = t >= annealing_sched.exit_t; + + return exit_anneal; + } + + /* Automatically determine exit temperature. */ + auto& cluster_ctx = g_vpr_ctx.clustering(); + float t_exit = 0.005 * costs.cost / cluster_ctx.clb_nlist.nets().size(); + + if (annealing_sched.type == e_sched_type::DUSTY_SCHED) { + /* May get nan if there are no nets */ + bool restart_temp = t < t_exit || std::isnan(t_exit); + + /* If the success rate or the temperature is * + * too low, reset the temperature and alpha. */ + if (success_rate < annealing_sched.success_min || restart_temp) { + /* Only exit anneal when alpha gets too large. */ + if (alpha > annealing_sched.alpha_max) { + return false; + } + /* Take a half step from the restart temperature. */ + t = restart_t / sqrt(alpha); + /* Update alpha. */ + alpha = 1.0 - ((1.0 - alpha) * annealing_sched.alpha_decay); + } else { + /* If the success rate is promising, next time * + * reset t to the current annealing temperature. */ + if (success_rate > annealing_sched.success_target) { + restart_t = t; + } + /* Update t. */ + t *= alpha; + } + + /* Update move lim. */ + update_move_lim(annealing_sched.success_target, success_rate); + } else { + VTR_ASSERT_SAFE(annealing_sched.type == e_sched_type::AUTO_SCHED); + /* Automatically adjust alpha according to success rate. */ + if (success_rate > 0.96) { + alpha = 0.5; + } else if (success_rate > 0.8) { + alpha = 0.9; + } else if (success_rate > 0.15 || rlim > 1.) { + alpha = 0.95; + } else { + alpha = 0.8; + } + /* Update temp. */ + t *= alpha; + /* Must be duplicated to retain previous behavior. */ + if (t < t_exit || std::isnan(t_exit)) { + return false; + } + } + + /* Update the range limiter. */ + update_rlim(success_rate); + + /* If using timing driven algorithm, update the crit_exponent. */ + if (placer_opts.place_algorithm.is_timing_driven()) { + update_crit_exponent(placer_opts); + } + + /* Continues the annealing. */ + return true; +} + +void t_annealing_state::update_rlim(float success_rate) { + rlim *= (1. - 0.44 + success_rate); + rlim = std::min(rlim, UPPER_RLIM); + rlim = std::max(rlim, FINAL_RLIM); +} + +void t_annealing_state::update_crit_exponent(const t_placer_opts& placer_opts) { + /* If rlim == FINAL_RLIM, then scale == 0. */ + float scale = 1 - (rlim - FINAL_RLIM) * INVERSE_DELTA_RLIM; + + /* Apply the scaling factor on crit_exponent. */ + crit_exponent = scale * (placer_opts.td_place_exp_last - placer_opts.td_place_exp_first) + + placer_opts.td_place_exp_first; +} + +void t_annealing_state::update_move_lim(float success_target, float success_rate) { + move_lim = move_lim_max * (success_target / success_rate); + move_lim = std::min(move_lim, move_lim_max); + move_lim = std::max(move_lim, 1); +} \ No newline at end of file diff --git a/vpr/src/place/annealer.h b/vpr/src/place/annealer.h new file mode 100644 index 00000000000..0c046bcdf5f --- /dev/null +++ b/vpr/src/place/annealer.h @@ -0,0 +1,135 @@ + +#pragma once + +class t_placer_costs; +struct t_placer_opts; +struct t_annealing_sched; + +/** + * @brief Stores variables that are used by the annealing process. + * + * This structure is updated by update_annealing_state() on each outer + * loop iteration. It stores various important variables that need to + * be accessed during the placement inner loop. + * + * Private variables are not given accessor functions. They serve as + * macros originally defined in place.cpp as global scope variables. + * + * Public members: + * @param t + * Temperature for simulated annealing. + * @param restart_t + * Temperature used after restart due to minimum success ratio. + * Currently only used and updated by DUSTY_SCHED. + * @param alpha + * Temperature decays factor (multiplied each outer loop iteration). + * @param num_temps + * The count of how many temperature iterations have passed. + * + * @param rlim + * Range limit for block swaps. + * Currently only updated by DUSTY_SCHED and AUTO_SCHED. + * @param crit_exponent + * Used by timing-driven placement to "sharpen" the timing criticality. + * Depends on rlim. Currently only updated by DUSTY_SCHED and AUTO_SCHED. + * @param move_lim + * Current block move limit. + * Currently only updated by DUSTY_SCHED. + * @param move_lim_max + * Maximum block move limit. + * + * Private members: + * @param UPPER_RLIM + * The upper limit for the range limiter value. + * @param FINAL_RLIM + * The final rlim (range limit) is 1, which is the smallest value that + * can still make progress, since an rlim of 0 wouldn't allow any swaps. + * @param INVERSE_DELTA_RLIM + * Used to update crit_exponent. See update_rlim() for more. + * + * Mutators: + * @param outer_loop_update() + * Update the annealing state variables in the placement outer loop. + * @param update_rlim(), update_crit_exponent(), update_move_lim() + * Inline subroutines used by the main routine outer_loop_update(). + */ +class t_annealing_state { + public: + float t; + float restart_t; + float alpha; + int num_temps; + + float rlim; + float crit_exponent; + int move_lim; + int move_lim_max; + + private: + float UPPER_RLIM; + float FINAL_RLIM = 1.; + float INVERSE_DELTA_RLIM; + + public: //Constructor + t_annealing_state() = delete; + t_annealing_state(const t_annealing_sched& annealing_sched, + float first_t, + float first_rlim, + int first_move_lim, + float first_crit_exponent); + + public: //Mutator + /** + * @brief Update the annealing state according to the annealing schedule selected. + * + * USER_SCHED: A manual fixed schedule with fixed alpha and exit criteria. + * AUTO_SCHED: A more sophisticated schedule where alpha varies based on success ratio. + * DUSTY_SCHED: This schedule jumps backward and slows down in response to success ratio. + * See doc/src/vpr/dusty_sa.rst for more details. + * + * @return True->continues the annealing. False->exits the annealing. + */ + bool outer_loop_update(float success_rate, + const t_placer_costs& costs, + const t_placer_opts& placer_opts, + const t_annealing_sched& annealing_sched); + + private: //Mutator + /** + * @brief Update the range limiter to keep acceptance prob. near 0.44. + * + * Use a floating point rlim to allow gradual transitions at low temps. + * The range is bounded by 1 (FINAL_RLIM) and the grid size (UPPER_RLIM). + */ + inline void update_rlim(float success_rate); + + /** + * @brief Update the criticality exponent. + * + * When rlim shrinks towards the FINAL_RLIM value (indicating + * that we are fine-tuning a more optimized placement), we can + * focus more on a smaller number of critical connections. + * To achieve this, we make the crit_exponent sharper, so that + * critical connections would become more critical than before. + * + * We calculate how close rlim is to its final value comparing + * to its initial value. Then, we apply the same scaling factor + * on the crit_exponent so that it lands on the suitable value + * between td_place_exp_first and td_place_exp_last. The scaling + * factor is calculated and applied linearly. + */ + inline void update_crit_exponent(const t_placer_opts& placer_opts); + + /** + * @brief Update the move limit based on the success rate. + * + * The value is bounded between 1 and move_lim_max. + */ + inline void update_move_lim(float success_target, float success_rate); +}; + +class PlacementAnnealer { + + private: + t_annealing_state annealing_state_; +}; \ No newline at end of file diff --git a/vpr/src/place/place.cpp b/vpr/src/place/place.cpp index aaa5620af50..90b566fb753 100644 --- a/vpr/src/place/place.cpp +++ b/vpr/src/place/place.cpp @@ -24,6 +24,7 @@ #include "globals.h" #include "place.h" +#include "annealer.h" #include "read_place.h" #include "draw.h" #include "place_and_route.h" @@ -688,8 +689,7 @@ void try_place(const Netlist<>& net_list, EPSILON, // Set the temperature low to ensure that initial placement quality will be preserved first_rlim, first_move_lim, - first_crit_exponent, - device_ctx.grid.get_num_layers()); + first_crit_exponent); /* Update the starting temperature for placement annealing to a more appropriate value */ state.t = starting_t(&state, &costs, annealing_sched, @@ -1173,7 +1173,7 @@ static float starting_t(const t_annealing_state* state, PlacerState& placer_state, NetCostHandler& net_cost_handler, std::optional& noc_cost_handler) { - if (annealing_sched.type == USER_SCHED) { + if (annealing_sched.type == e_sched_type::USER_SCHED) { return (annealing_sched.init_t); } diff --git a/vpr/src/place/place_util.cpp b/vpr/src/place/place_util.cpp index ce24914b7f2..ec7ecb8982e 100644 --- a/vpr/src/place/place_util.cpp +++ b/vpr/src/place/place_util.cpp @@ -71,38 +71,6 @@ t_placer_costs& t_placer_costs::operator+=(const NocCostTerms& noc_delta_cost) { return *this; } -///@brief Constructor: Initialize all annealing state variables and macros. -t_annealing_state::t_annealing_state(const t_annealing_sched& annealing_sched, - float first_t, - float first_rlim, - int first_move_lim, - float first_crit_exponent, - int num_laters) { - num_temps = 0; - alpha = annealing_sched.alpha_min; - t = first_t; - restart_t = first_t; - rlim = first_rlim; - move_lim_max = first_move_lim; - crit_exponent = first_crit_exponent; - - /* Determine the current move_lim based on the schedule type */ - if (annealing_sched.type == DUSTY_SCHED) { - move_lim = std::max(1, (int)(move_lim_max * annealing_sched.success_target)); - } else { - move_lim = move_lim_max; - } - - NUM_LAYERS = num_laters; - - /* Store this inverse value for speed when updating crit_exponent. */ - INVERSE_DELTA_RLIM = 1 / (first_rlim - FINAL_RLIM); - - /* The range limit cannot exceed the largest grid size. */ - auto& grid = g_vpr_ctx.device().grid; - UPPER_RLIM = std::max(grid.width() - 1, grid.height() - 1); -} - int get_initial_move_lim(const t_placer_opts& placer_opts, const t_annealing_sched& annealing_sched) { const auto& device_ctx = g_vpr_ctx.device(); const auto& cluster_ctx = g_vpr_ctx.clustering(); @@ -126,112 +94,6 @@ int get_initial_move_lim(const t_placer_opts& placer_opts, const t_annealing_sch return move_lim; } -bool t_annealing_state::outer_loop_update(float success_rate, - const t_placer_costs& costs, - const t_placer_opts& placer_opts, - const t_annealing_sched& annealing_sched) { -#ifndef NO_GRAPHICS - t_draw_state* draw_state = get_draw_state_vars(); - if (!draw_state->list_of_breakpoints.empty()) { - /* Update temperature in the current information variable. */ - get_bp_state_globals()->get_glob_breakpoint_state()->temp_count++; - } -#endif - - if (annealing_sched.type == USER_SCHED) { - /* Update t with user specified alpha. */ - t *= annealing_sched.alpha_t; - - /* Check if the exit criterion is met. */ - bool exit_anneal = t >= annealing_sched.exit_t; - - return exit_anneal; - } - - /* Automatically determine exit temperature. */ - auto& cluster_ctx = g_vpr_ctx.clustering(); - float t_exit = 0.005 * costs.cost / cluster_ctx.clb_nlist.nets().size(); - - if (annealing_sched.type == DUSTY_SCHED) { - /* May get nan if there are no nets */ - bool restart_temp = t < t_exit || std::isnan(t_exit); - - /* If the success rate or the temperature is * - * too low, reset the temperature and alpha. */ - if (success_rate < annealing_sched.success_min || restart_temp) { - /* Only exit anneal when alpha gets too large. */ - if (alpha > annealing_sched.alpha_max) { - return false; - } - /* Take a half step from the restart temperature. */ - t = restart_t / sqrt(alpha); - /* Update alpha. */ - alpha = 1.0 - ((1.0 - alpha) * annealing_sched.alpha_decay); - } else { - /* If the success rate is promising, next time * - * reset t to the current annealing temperature. */ - if (success_rate > annealing_sched.success_target) { - restart_t = t; - } - /* Update t. */ - t *= alpha; - } - - /* Update move lim. */ - update_move_lim(annealing_sched.success_target, success_rate); - } else { - VTR_ASSERT_SAFE(annealing_sched.type == AUTO_SCHED); - /* Automatically adjust alpha according to success rate. */ - if (success_rate > 0.96) { - alpha = 0.5; - } else if (success_rate > 0.8) { - alpha = 0.9; - } else if (success_rate > 0.15 || rlim > 1.) { - alpha = 0.95; - } else { - alpha = 0.8; - } - /* Update temp. */ - t *= alpha; - /* Must be duplicated to retain previous behavior. */ - if (t < t_exit || std::isnan(t_exit)) { - return false; - } - } - - /* Update the range limiter. */ - update_rlim(success_rate); - - /* If using timing driven algorithm, update the crit_exponent. */ - if (placer_opts.place_algorithm.is_timing_driven()) { - update_crit_exponent(placer_opts); - } - - /* Continues the annealing. */ - return true; -} - -void t_annealing_state::update_rlim(float success_rate) { - rlim *= (1. - 0.44 + success_rate); - rlim = std::min(rlim, UPPER_RLIM); - rlim = std::max(rlim, FINAL_RLIM); -} - -void t_annealing_state::update_crit_exponent(const t_placer_opts& placer_opts) { - /* If rlim == FINAL_RLIM, then scale == 0. */ - float scale = 1 - (rlim - FINAL_RLIM) * INVERSE_DELTA_RLIM; - - /* Apply the scaling factor on crit_exponent. */ - crit_exponent = scale * (placer_opts.td_place_exp_last - placer_opts.td_place_exp_first) - + placer_opts.td_place_exp_first; -} - -void t_annealing_state::update_move_lim(float success_target, float success_rate) { - move_lim = move_lim_max * (success_target / success_rate); - move_lim = std::min(move_lim, move_lim_max); - move_lim = std::max(move_lim, 1); -} - ///@brief Clear all data fields. void t_placer_statistics::reset() { av_cost = 0.; @@ -390,7 +252,7 @@ bool macro_can_be_placed(const t_pl_macro& pl_macro, } } - return (mac_can_be_placed); + return mac_can_be_placed; } NocCostTerms::NocCostTerms(double agg_bw, double lat, double lat_overrun, double congest) diff --git a/vpr/src/place/place_util.h b/vpr/src/place/place_util.h index 60d4a86b1c5..49f4246dbe5 100644 --- a/vpr/src/place/place_util.h +++ b/vpr/src/place/place_util.h @@ -126,130 +126,6 @@ class t_placer_costs { t_place_algorithm place_algorithm; }; -/** - * @brief Stores variables that are used by the annealing process. - * - * This structure is updated by update_annealing_state() on each outer - * loop iteration. It stores various important variables that need to - * be accessed during the placement inner loop. - * - * Private variables are not given accessor functions. They serve as - * macros originally defined in place.cpp as global scope variables. - * - * Public members: - * @param t - * Temperature for simulated annealing. - * @param restart_t - * Temperature used after restart due to minimum success ratio. - * Currently only used and updated by DUSTY_SCHED. - * @param alpha - * Temperature decays factor (multiplied each outer loop iteration). - * @param num_temps - * The count of how many temperature iterations have passed. - * - * @param rlim - * Range limit for block swaps. - * Currently only updated by DUSTY_SCHED and AUTO_SCHED. - * @param crit_exponent - * Used by timing-driven placement to "sharpen" the timing criticality. - * Depends on rlim. Currently only updated by DUSTY_SCHED and AUTO_SCHED. - * @param move_lim - * Current block move limit. - * Currently only updated by DUSTY_SCHED. - * @param move_lim_max - * Maximum block move limit. - * - * Private members: - * @param UPPER_RLIM - * The upper limit for the range limiter value. - * @param FINAL_RLIM - * The final rlim (range limit) is 1, which is the smallest value that - * can still make progress, since an rlim of 0 wouldn't allow any swaps. - * @param INVERSE_DELTA_RLIM - * Used to update crit_exponent. See update_rlim() for more. - * - * Mutators: - * @param outer_loop_update() - * Update the annealing state variables in the placement outer loop. - * @param update_rlim(), update_crit_exponent(), update_move_lim() - * Inline subroutines used by the main routine outer_loop_update(). - */ -class t_annealing_state { - public: - float t; - float restart_t; - float alpha; - int num_temps; - - float rlim; - float crit_exponent; - int move_lim; - int move_lim_max; - - private: - float UPPER_RLIM; - float FINAL_RLIM = 1.; - float INVERSE_DELTA_RLIM; - int NUM_LAYERS = 1; - - public: //Constructor - t_annealing_state(const t_annealing_sched& annealing_sched, - float first_t, - float first_rlim, - int first_move_lim, - float first_crit_exponent, - int num_layers); - - public: //Mutator - /** - * @brief Update the annealing state according to the annealing schedule selected. - * - * USER_SCHED: A manual fixed schedule with fixed alpha and exit criteria. - * AUTO_SCHED: A more sophisticated schedule where alpha varies based on success ratio. - * DUSTY_SCHED: This schedule jumps backward and slows down in response to success ratio. - * See doc/src/vpr/dusty_sa.rst for more details. - * - * @return True->continues the annealing. False->exits the annealing. - */ - bool outer_loop_update(float success_rate, - const t_placer_costs& costs, - const t_placer_opts& placer_opts, - const t_annealing_sched& annealing_sched); - - private: //Mutator - /** - * @brief Update the range limiter to keep acceptance prob. near 0.44. - * - * Use a floating point rlim to allow gradual transitions at low temps. - * The range is bounded by 1 (FINAL_RLIM) and the grid size (UPPER_RLIM). - */ - inline void update_rlim(float success_rate); - - /** - * @brief Update the criticality exponent. - * - * When rlim shrinks towards the FINAL_RLIM value (indicating - * that we are fine-tuning a more optimized placement), we can - * focus more on a smaller number of critical connections. - * To achieve this, we make the crit_exponent sharper, so that - * critical connections would become more critical than before. - * - * We calculate how close rlim is to its final value comparing - * to its initial value. Then, we apply the same scaling factor - * on the crit_exponent so that it lands on the suitable value - * between td_place_exp_first and td_place_exp_last. The scaling - * factor is calculated and applied linearly. - */ - inline void update_crit_exponent(const t_placer_opts& placer_opts); - - /** - * @brief Update the move limit based on the success rate. - * - * The value is bounded between 1 and move_lim_max. - */ - inline void update_move_lim(float success_target, float success_rate); -}; - /** * @brief Stores statistics produced by a single annealing iteration. * From 32a2726602c4471372a80c3a84dfc22891e84bf7 Mon Sep 17 00:00:00 2001 From: soheilshahrouz Date: Tue, 15 Oct 2024 19:33:47 -0400 Subject: [PATCH 028/162] commit before I go home. --- vpr/src/base/vpr_types.h | 2 +- vpr/src/place/annealer.cpp | 911 +++++++++++++++++++++++++++++++++++ vpr/src/place/annealer.h | 104 +++- vpr/src/place/move_utils.h | 12 - vpr/src/place/place.cpp | 462 +----------------- vpr/src/place/place.h | 1 - vpr/src/place/placer_state.h | 8 + 7 files changed, 1032 insertions(+), 468 deletions(-) diff --git a/vpr/src/base/vpr_types.h b/vpr/src/base/vpr_types.h index 84432ed7181..cb280ff36ec 100644 --- a/vpr/src/base/vpr_types.h +++ b/vpr/src/base/vpr_types.h @@ -1066,6 +1066,7 @@ enum class e_move_type; struct t_placer_opts { t_place_algorithm place_algorithm; t_place_algorithm place_quench_algorithm; + t_annealing_sched anneal_sched; ///name, (to_type ? to_type->name : "EMPTY"), \ + affected_blocks.moved_blocks.size()); \ + } \ + } while (false) + +# define LOG_MOVE_STATS_OUTCOME(delta_cost, delta_bb_cost, delta_td_cost, \ + outcome, reason) \ + do { \ + if (f_move_stats_file) { \ + fprintf(f_move_stats_file.get(), \ + "%g,%g,%g," \ + "%s,%s\n", \ + delta_cost, delta_bb_cost, delta_td_cost, \ + outcome, reason); \ + } \ + } while (false) + +#else + +# define LOG_MOVE_STATS_HEADER() \ + do { \ + fprintf(move_stats_file_.get(), \ + "VTR_ENABLE_DEBUG_LOGGING disabled " \ + "-- No move stats recorded\n"); \ + } while (false) + +# define LOG_MOVE_STATS_PROPOSED(t, blocks_affected) \ + do { \ + } while (false) + +# define LOG_MOVE_STATS_OUTCOME(delta_cost, delta_bb_cost, delta_td_cost, \ + outcome, reason) \ + do { \ + } while (false) + +#endif + + +/** + * @brief Invalidates the connections affected by the specified block moves. + * + * All the connections recorded in blocks_affected.affected_pins have different + * values for `proposed_connection_delay` and `connection_delay`. + * + * Invalidate all the timing graph edges associated with these connections via + * the NetPinTimingInvalidator class. + */ +static void invalidate_affected_connections(const t_pl_blocks_to_be_moved& blocks_affected, + NetPinTimingInvalidator* pin_tedges_invalidator, + TimingInfo* timing_info); + +/** + * @brief Update the connection_timing_cost values from the temporary + * values for all connections that have/haven't changed. + * + * All the connections have already been gathered by blocks_affected.affected_pins + * after running the routine find_affected_nets_and_update_costs() in try_swap(). + */ +static void commit_td_cost(const t_pl_blocks_to_be_moved& blocks_affected, + PlacerState& placer_state); + +/** + * @brief Check if the setup slack has gotten better or worse due to block swap. + * + * Get all the modified slack values via the PlacerSetupSlacks class, and compare + * then with the original values at these connections. Sort them and compare them + * one by one, and return the difference of the first different pair. + * + * If the new slack value is larger(better), than return a negative value so that + * the move will be accepted. If the new slack value is smaller(worse), return a + * positive value so that the move will be rejected. + * + * If no slack values have changed, then return an arbitrary positive number. A + * move resulting in no change in the slack values should probably be unnecessary. + * + * The sorting is need to prevent in the unlikely circumstances that a bad slack + * value suddenly got very good due to the block move, while a good slack value + * got very bad, perhaps even worse than the original worse slack value. + */ +static float analyze_setup_slack_cost(const PlacerSetupSlacks* setup_slacks, + const PlacerState& placer_state); + +static e_move_result assess_swap(double delta_c, double t); + +static void invalidate_affected_connections(const t_pl_blocks_to_be_moved& blocks_affected, + NetPinTimingInvalidator* pin_tedges_invalidator, + TimingInfo* timing_info) { + VTR_ASSERT_SAFE(timing_info); + VTR_ASSERT_SAFE(pin_tedges_invalidator); + + // Invalidate timing graph edges affected by the move + for (ClusterPinId pin : blocks_affected.affected_pins) { + pin_tedges_invalidator->invalidate_connection(pin, timing_info); + } +} + +static void commit_td_cost(const t_pl_blocks_to_be_moved& blocks_affected, + PlacerState& placer_state) { + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& clb_nlist = cluster_ctx.clb_nlist; + + auto& p_timing_ctx = placer_state.mutable_timing(); + auto& connection_delay = p_timing_ctx.connection_delay; + auto& proposed_connection_delay = p_timing_ctx.proposed_connection_delay; + auto& connection_timing_cost = p_timing_ctx.connection_timing_cost; + auto& proposed_connection_timing_cost = p_timing_ctx.proposed_connection_timing_cost; + + //Go through all the sink pins affected + for (ClusterPinId pin_id : blocks_affected.affected_pins) { + ClusterNetId net_id = clb_nlist.pin_net(pin_id); + int ipin = clb_nlist.pin_net_index(pin_id); + + //Commit the timing delay and cost values + connection_delay[net_id][ipin] = proposed_connection_delay[net_id][ipin]; + proposed_connection_delay[net_id][ipin] = INVALID_DELAY; + connection_timing_cost[net_id][ipin] = proposed_connection_timing_cost[net_id][ipin]; + proposed_connection_timing_cost[net_id][ipin] = INVALID_DELAY; + } +} + +static float analyze_setup_slack_cost(const PlacerSetupSlacks* setup_slacks, + const PlacerState& placer_state) { + const auto& cluster_ctx = g_vpr_ctx.clustering(); + const auto& clb_nlist = cluster_ctx.clb_nlist; + + const auto& p_timing_ctx = placer_state.timing(); + const auto& connection_setup_slack = p_timing_ctx.connection_setup_slack; + + //Find the original/proposed setup slacks of pins with modified values + std::vector original_setup_slacks, proposed_setup_slacks; + + auto clb_pins_modified = setup_slacks->pins_with_modified_setup_slack(); + for (ClusterPinId clb_pin : clb_pins_modified) { + ClusterNetId net_id = clb_nlist.pin_net(clb_pin); + size_t ipin = clb_nlist.pin_net_index(clb_pin); + + original_setup_slacks.push_back(connection_setup_slack[net_id][ipin]); + proposed_setup_slacks.push_back( + setup_slacks->setup_slack(net_id, ipin)); + } + + //Sort in ascending order, from the worse slack value to the best + std::stable_sort(original_setup_slacks.begin(), original_setup_slacks.end()); + std::stable_sort(proposed_setup_slacks.begin(), proposed_setup_slacks.end()); + + //Check the first pair of slack values that are different + //If found, return their difference + for (size_t idiff = 0; idiff < original_setup_slacks.size(); ++idiff) { + float slack_diff = original_setup_slacks[idiff] + - proposed_setup_slacks[idiff]; + + if (slack_diff != 0) { + return slack_diff; + } + } + + //If all slack values are identical (or no modified slack values), + //reject this move by returning an arbitrary positive number as cost. + return 1; +} + +static e_move_result assess_swap(double delta_c, double t) { + /* Returns: 1 -> move accepted, 0 -> rejected. */ + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\tTemperature is: %e delta_c is %e\n", t, delta_c); + if (delta_c <= 0) { + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is accepted(delta_c < 0)\n"); + return ACCEPTED; + } + + if (t == 0.) { + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is rejected(t == 0)\n"); + return REJECTED; + } + + float fnum = vtr::frand(); + float prob_fac = std::exp(-delta_c / t); + if (prob_fac > fnum) { + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is accepted(hill climbing)\n"); + return ACCEPTED; + } + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is rejected(hill climbing)\n"); + return REJECTED; +} + +//Reverts modifications to proposed_connection_delay and proposed_connection_timing_cost based on +//the move proposed in blocks_affected +static void revert_td_cost(const t_pl_blocks_to_be_moved& blocks_affected, + PlacerTimingContext& p_timing_ctx) { +#ifndef VTR_ASSERT_SAFE_ENABLED + (void)blocks_affected; + (void)p_timing_ctx; +#else + //Invalidate temp delay & timing cost values to match sanity checks in + //comp_td_connection_cost() + auto& cluster_ctx = g_vpr_ctx.clustering(); + auto& clb_nlist = cluster_ctx.clb_nlist; + + auto& proposed_connection_delay = p_timing_ctx.proposed_connection_delay; + auto& proposed_connection_timing_cost = p_timing_ctx.proposed_connection_timing_cost; + + for (ClusterPinId pin : blocks_affected.affected_pins) { + ClusterNetId net = clb_nlist.pin_net(pin); + int ipin = clb_nlist.pin_net_index(pin); + proposed_connection_delay[net][ipin] = INVALID_DELAY; + proposed_connection_timing_cost[net][ipin] = INVALID_DELAY; + } +#endif +} + +/** + * @brief Compute the total normalized cost for a given placement. This + * computation will vary depending on the placement modes. + * + * @param costs The current placement cost components and their normalization + * factors + * @param placer_opts Determines the placement mode + * @param noc_opts Determines if placement includes the NoC + * @return double The computed total cost of the current placement + */ +static double get_total_cost(t_placer_costs* costs, const t_placer_opts& placer_opts, const t_noc_opts& noc_opts) { + double total_cost = 0.0; + + if (placer_opts.place_algorithm == BOUNDING_BOX_PLACE) { + // in bounding box mode we only care about wirelength + total_cost = costs->bb_cost * costs->bb_cost_norm; + } else if (placer_opts.place_algorithm.is_timing_driven()) { + // in timing mode we include both wirelength and timing costs + total_cost = (1 - placer_opts.timing_tradeoff) * (costs->bb_cost * costs->bb_cost_norm) + (placer_opts.timing_tradeoff) * (costs->timing_cost * costs->timing_cost_norm); + } + + if (noc_opts.noc) { + // in noc mode we include noc aggregate bandwidth and noc latency + total_cost += calculate_noc_cost(costs->noc_cost_terms, costs->noc_cost_norm_factors, noc_opts); + } + + return total_cost; +} + +/** + * @brief Updates all the cost normalization factors during the outer + * loop iteration of the placement. At each temperature change, these + * values are updated so that we can balance the tradeoff between the + * different placement cost components (timing, wirelength and NoC). + * Depending on the placement mode the corresponding normalization factors are + * updated. + * + * @param costs Contains the normalization factors which need to be updated + * @param placer_opts Determines the placement mode + * @param noc_opts Determines if placement includes the NoC + * @param noc_cost_handler Computes normalization factors for NoC-related cost terms + */ +static void update_placement_cost_normalization_factors(t_placer_costs* costs, + const t_placer_opts& placer_opts, + const t_noc_opts& noc_opts, + const std::optional& noc_cost_handler) { + /* Update the cost normalization factors */ + costs->update_norm_factors(); + + // update the noc normalization factors if the placement includes the NoC + if (noc_opts.noc) { + noc_cost_handler->update_noc_normalization_factors(*costs); + } + + // update the current total placement cost + costs->cost = get_total_cost(costs, placer_opts, noc_opts); +} ///@brief Constructor: Initialize all annealing state variables and macros. t_annealing_state::t_annealing_state(const t_annealing_sched& annealing_sched, @@ -142,4 +450,607 @@ void t_annealing_state::update_move_lim(float success_target, float success_rate move_lim = move_lim_max * (success_target / success_rate); move_lim = std::min(move_lim, move_lim_max); move_lim = std::max(move_lim, 1); +} + +PlacementAnnealer::PlacementAnnealer(const t_placer_opts& placer_opts, + PlacerState& placer_state, + t_placer_costs& costs, + NetCostHandler& net_cost_handler, + std::optional& noc_cost_handler, + const t_noc_opts& noc_opts, + MoveGenerator& move_generator_1, + MoveGenerator& move_generator_2, + ManualMoveGenerator& manual_move_generator, + const PlaceDelayModel* delay_model, + PlacerCriticalities* criticalities, + PlacerSetupSlacks* setup_slacks, + SetupTimingInfo* timing_info, + NetPinTimingInvalidator* pin_timing_invalidator, + int move_lim) + : placer_opts_(placer_opts) + , placer_state_(placer_state) + , costs_(costs) + , net_cost_handler_(net_cost_handler) + , noc_cost_handler_(noc_cost_handler) + , noc_opts_(noc_opts) + , move_generator_1_(move_generator_1) + , move_generator_2_(move_generator_2) + , manual_move_generator_(manual_move_generator) + , delay_model_(delay_model) + , criticalities_(criticalities) + , setup_slacks_(setup_slacks) + , timing_info_(timing_info) + , pin_timing_invalidator_(pin_timing_invalidator) + , move_stats_file_(nullptr, vtr::fclose) + , outer_crit_iter_count_(1) + , blocks_affected_(placer_state.block_locs().size()) +{ + const auto& device_ctx = g_vpr_ctx.device(); + + float first_crit_exponent; + if (placer_opts.place_algorithm.is_timing_driven()) { + first_crit_exponent = placer_opts.td_place_exp_first; /*this will be modified when rlim starts to change */ + } else { + first_crit_exponent = 0.f; + } + + int first_move_lim = get_initial_move_lim(placer_opts, placer_opts_.anneal_sched); + + int inner_recompute_limit; + if (placer_opts.inner_loop_recompute_divider != 0) { + inner_recompute_limit = static_cast(0.5 + (float)first_move_lim / (float)placer_opts.inner_loop_recompute_divider); + } else { + // don't do an inner recompute + inner_recompute_limit = first_move_lim + 1; + } + + /* calculate the number of moves in the quench that we should recompute timing after based on the value of * + * the commandline option quench_recompute_divider */ + int quench_recompute_limit; + if (placer_opts.quench_recompute_divider != 0) { + quench_recompute_limit = static_cast(0.5 + (float)move_lim / (float)placer_opts.quench_recompute_divider); + } else { + /*don't do an quench recompute */ + quench_recompute_limit = first_move_lim + 1; + } + + // Get the first range limiter + placer_state_.mutable_move().first_rlim = (float)std::max(device_ctx.grid.width() - 1, device_ctx.grid.height() - 1); + + annealing_state_ = t_annealing_state(placer_opts_.anneal_sched, + EPSILON, // Set the temperature low to ensure that initial placement quality will be preserved + placer_state_.move().first_rlim, + first_move_lim, + first_crit_exponent); + + if (!placer_opts.move_stats_file.empty()) { + move_stats_file_ = std::unique_ptr( + vtr::fopen(placer_opts.move_stats_file.c_str(), "w"), + vtr::fclose); + LOG_MOVE_STATS_HEADER(); + } + + //allocate move type statistics vectors + move_type_stats_.blk_type_moves.resize({device_ctx.logical_block_types.size(), (int)e_move_type::NUMBER_OF_AUTO_MOVES}, 0); + move_type_stats_.accepted_moves.resize({device_ctx.logical_block_types.size(), (int)e_move_type::NUMBER_OF_AUTO_MOVES}, 0); + move_type_stats_.rejected_moves.resize({device_ctx.logical_block_types.size(), (int)e_move_type::NUMBER_OF_AUTO_MOVES}, 0); + + // Update the starting temperature for placement annealing to a more appropriate value + annealing_state_.t = estimate_starting_temperature(); +} + +float PlacementAnnealer::estimate_starting_temperature() { + if (placer_opts_.anneal_sched.type == e_sched_type::USER_SCHED) { + return placer_opts_.anneal_sched.init_t; + } + + const auto& cluster_ctx = g_vpr_ctx.clustering(); + + // Use to calculate the average of cost when swap is accepted. + int num_accepted = 0; + + // Use double types to avoid round off. + double av = 0., sum_of_squares = 0.; + + // Determines the block swap loop count. + int move_lim = std::min(annealing_state_.move_lim_max, (int)cluster_ctx.clb_nlist.blocks().size()); + + bool manual_move_enabled = false; + + for (int i = 0; i < move_lim; i++) { +#ifndef NO_GRAPHICS + // Checks manual move flag for manual move feature + t_draw_state* draw_state = get_draw_state_vars(); + if (draw_state->show_graphics) { + manual_move_enabled = manual_move_is_selected(); + } +#endif /*NO_GRAPHICS*/ + + // TODO: remove this + constexpr float REWARD_BB_TIMING_RELATIVE_WEIGHT = 0.4; + + // Will not deploy setup slack analysis, so omit crit_exponenet and setup_slack + e_move_result swap_result = try_swap(move_generator_1_, placer_opts_.place_algorithm, + REWARD_BB_TIMING_RELATIVE_WEIGHT, manual_move_enabled); + + if (swap_result == ACCEPTED) { + num_accepted++; + av += costs_.cost; + sum_of_squares += costs_.cost * costs_.cost; + swap_stats_.num_swap_accepted++; + } else if (swap_result == ABORTED) { + swap_stats_.num_swap_aborted++; + } else { + swap_stats_.num_swap_rejected++; + } + } + + // Take the average of the accepted swaps' cost values. + av = num_accepted > 0 ? (av / num_accepted) : 0.; + + // Get the standard deviation. + double std_dev = get_std_dev(num_accepted, sum_of_squares, av); + + // Print warning if not all swaps are accepted. + if (num_accepted != move_lim) { + VTR_LOG_WARN("Starting t: %d of %d configurations accepted.\n", + num_accepted, move_lim); + } + +#ifdef VERBOSE + /* Print stats related to finding the initital temp. */ + VTR_LOG("std_dev: %g, average cost: %g, starting temp: %g\n", std_dev, av, 20. * std_dev); +#endif + + // Improved initial placement uses a fast SA for NoC routers and centroid placement + // for other blocks. The temperature is reduced to prevent SA from destroying the initial placement + float init_temp = std_dev / 64; + + return init_temp; +} + + +/** + * @brief Pick some block and moves it to another spot. + * + * If the new location is empty, directly move the block. If the new location + * is occupied, switch the blocks. Due to the different sizes of the blocks, + * this block switching may occur for multiple times. It might also cause the + * current swap attempt to abort due to inability to find suitable locations + * for moved blocks. + * + * The move generator will record all the switched blocks in the variable + * `blocks_affected`. Afterwards, the move will be assessed by the chosen + * cost formulation. Currently, there are three ways to assess move cost, + * which are stored in the enum type `t_place_algorithm`. + * + * @return Whether the block swap is accepted, rejected or aborted. + */ +e_move_result PlacementAnnealer::try_swap(MoveGenerator& move_generator, + const t_place_algorithm& place_algorithm, + float timing_bb_factor, + bool manual_move_enabled) { + /* Picks some block and moves it to another spot. If this spot is + * occupied, switch the blocks. Assess the change in cost function. + * rlim is the range limiter. + * Returns whether the swap is accepted, rejected or aborted. + * Passes back the new value of the cost functions. + */ + auto& blk_loc_registry = placer_state_.mutable_blk_loc_registry(); + + float rlim_escape_fraction = placer_opts_.rlim_escape_fraction; + float timing_tradeoff = placer_opts_.timing_tradeoff; + + PlaceCritParams crit_params; + crit_params.crit_exponent = annealing_state_.crit_exponent; + crit_params.crit_limit = placer_opts_.place_crit_limit; + + // move type and block type chosen by the agent + t_propose_action proposed_action{e_move_type::UNIFORM, -1}; + + swap_stats_.num_ts_called++; + + MoveOutcomeStats move_outcome_stats; + + /* I'm using negative values of proposed_net_cost as a flag, + * so DO NOT use cost functions that can go negative. */ + + double delta_c = 0; //Change in cost due to this swap. + double bb_delta_c = 0; //Change in the bounding box (wiring) cost. + double timing_delta_c = 0; //Change in the timing cost (delay * criticality). + + // Determine whether we need to force swap two router blocks + bool router_block_move = false; + if (noc_opts_.noc) { + router_block_move = check_for_router_swap(noc_opts_.noc_swap_percentage); + } + + /* Allow some fraction of moves to not be restricted by rlim, + /* in the hopes of better escaping local minima. */ + float rlim; + if (rlim_escape_fraction > 0. && vtr::frand() < rlim_escape_fraction) { + rlim = std::numeric_limits::infinity(); + } else { + rlim = annealing_state_.rlim; + } + + e_create_move create_move_outcome = e_create_move::ABORT; + + //When manual move toggle button is active, the manual move window asks the user for input. + if (manual_move_enabled) { +#ifndef NO_GRAPHICS + create_move_outcome = manual_move_display_and_propose(manual_move_generator_, blocks_affected_, + proposed_action.move_type, rlim, placer_opts_, + criticalities_); +#endif //NO_GRAPHICS + } else if (router_block_move) { + // generate a move where two random router blocks are swapped + create_move_outcome = propose_router_swap(blocks_affected_, rlim, blk_loc_registry); + proposed_action.move_type = e_move_type::UNIFORM; + } else { + //Generate a new move (perturbation) used to explore the space of possible placements + create_move_outcome = move_generator.propose_move(blocks_affected_, proposed_action, rlim, placer_opts_, criticalities_); + } + + if (proposed_action.logical_blk_type_index != -1) { //if the agent proposed the block type, then collect the block type stat + ++move_type_stats_.blk_type_moves[proposed_action.logical_blk_type_index][(int)proposed_action.move_type]; + } + LOG_MOVE_STATS_PROPOSED(t, blocks_affected_); + + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, + "\t\tBefore move Place cost %e, bb_cost %e, timing cost %e\n", + costs_.cost, costs_.bb_cost, costs_.timing_cost); + + e_move_result move_outcome = e_move_result::ABORTED; + + if (create_move_outcome == e_create_move::ABORT) { + LOG_MOVE_STATS_OUTCOME(std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN(), "ABORTED", + "illegal move"); + + move_outcome = ABORTED; + + } else { + VTR_ASSERT(create_move_outcome == e_create_move::VALID); + + /* + * To make evaluating the move simpler (e.g. calculating changed bounding box), + * we first move the blocks to their new locations (apply the move to + * blk_loc_registry.block_locs) and then compute the change in cost. If the move + * is accepted, the inverse look-up in place_ctx.grid_blocks is updated + * (committing the move). If the move is rejected, the blocks are returned to + * their original positions (reverting blk_loc_registry.block_locs to its original state). + * + * Note that the inverse look-up place_ctx.grid_blocks is only updated after + * move acceptance is determined, so it should not be used when evaluating a move. + */ + + /* Update the block positions */ + blk_loc_registry.apply_move_blocks(blocks_affected_); + + //Find all the nets affected by this swap and update the wiring costs. + //This cost value doesn't depend on the timing info. + // + //Also find all the pins affected by the swap, and calculates new connection + //delays and timing costs and store them in proposed_* data structures. + net_cost_handler_.find_affected_nets_and_update_costs(delay_model_, criticalities_, blocks_affected_, + bb_delta_c, timing_delta_c); + + //For setup slack analysis, we first do a timing analysis to get the newest + //slack values resulted from the proposed block moves. If the move turns out + //to be accepted, we keep the updated slack values and commit the block moves. + //If rejected, we reject the proposed block moves and revert this timing analysis. + if (place_algorithm == SLACK_TIMING_PLACE) { + // Invalidates timing of modified connections for incremental timing updates. + invalidate_affected_connections(blocks_affected_, pin_timing_invalidator_, timing_info_); + + /* Update the connection_timing_cost and connection_delay * + * values from the temporary values. */ + commit_td_cost(blocks_affected_, placer_state_); + + /* Update timing information. Since we are analyzing setup slacks, * + * we only update those values and keep the criticalities stale * + * so as not to interfere with the original timing driven algorithm. * + * + * Note: the timing info must be updated after applying block moves * + * and committing the timing driven delays and costs. * + * If we wish to revert this timing update due to move rejection, * + * we need to revert block moves and restore the timing values. */ + criticalities_->disable_update(); + setup_slacks_->enable_update(); + update_timing_classes(crit_params, timing_info_, criticalities_, + setup_slacks_, pin_timing_invalidator_, placer_state_); + + /* Get the setup slack analysis cost */ + //TODO: calculate a weighted average of the slack cost and wiring cost + delta_c = analyze_setup_slack_cost(setup_slacks_, placer_state_) * costs_.timing_cost_norm; + } else if (place_algorithm == CRITICALITY_TIMING_PLACE) { + /* Take delta_c as a combination of timing and wiring cost. In + * addition to `timing_tradeoff`, we normalize the cost values */ + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, + "\t\tMove bb_delta_c %e, bb_cost_norm %e, timing_tradeoff %f, " + "timing_delta_c %e, timing_cost_norm %e\n", + bb_delta_c, + costs_.bb_cost_norm, + timing_tradeoff, + timing_delta_c, + costs_.timing_cost_norm); + delta_c = (1 - timing_tradeoff) * bb_delta_c * costs_.bb_cost_norm + + timing_tradeoff * timing_delta_c * costs_.timing_cost_norm; + } else { + VTR_ASSERT_SAFE(place_algorithm == BOUNDING_BOX_PLACE); + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, + "\t\tMove bb_delta_c %e, bb_cost_norm %e\n", + bb_delta_c, + costs_.bb_cost_norm); + delta_c = bb_delta_c * costs_.bb_cost_norm; + } + + NocCostTerms noc_delta_c; // change in NoC cost + /* Update the NoC data structure and costs*/ + if (noc_opts_.noc) { + VTR_ASSERT_SAFE(noc_cost_handler_.has_value()); + noc_cost_handler_->find_affected_noc_routers_and_update_noc_costs(blocks_affected_, noc_delta_c); + + // Include the NoC delta costs in the total cost change for this swap + delta_c += calculate_noc_cost(noc_delta_c, costs_.noc_cost_norm_factors, noc_opts_); + } + + /* 1 -> move accepted, 0 -> rejected. */ + move_outcome = assess_swap(delta_c, annealing_state_.t); + + //Updates the manual_move_state members and displays costs to the user to decide whether to ACCEPT/REJECT manual move. +#ifndef NO_GRAPHICS + if (manual_move_enabled) { + move_outcome = pl_do_manual_move(delta_c, timing_delta_c, bb_delta_c, move_outcome); + } +#endif //NO_GRAPHICS + + if (move_outcome == ACCEPTED) { + costs_.cost += delta_c; + costs_.bb_cost += bb_delta_c; + + if (place_algorithm == SLACK_TIMING_PLACE) { + // Update the timing driven cost as usual + costs_.timing_cost += timing_delta_c; + + // Commit the setup slack information + // The timing delay and cost values should be committed already + commit_setup_slacks(setup_slacks_, placer_state_); + } + + if (place_algorithm == CRITICALITY_TIMING_PLACE) { + costs_.timing_cost += timing_delta_c; + + /* Invalidates timing of modified connections for incremental * + * timing updates. These invalidations are accumulated for a * + * big timing update in the outer loop. */ + invalidate_affected_connections(blocks_affected_, pin_timing_invalidator_, timing_info_); + + /* Update the connection_timing_cost and connection_delay * + * values from the temporary values. */ + commit_td_cost(blocks_affected_, placer_state_); + } + + /* Update net cost functions and reset flags. */ + net_cost_handler_.update_move_nets(); + + /* Update clb data structures since we kept the move. */ + blk_loc_registry.commit_move_blocks(blocks_affected_); + + if (proposed_action.logical_blk_type_index != -1) { //if the agent proposed the block type, then collect the block type stat + ++move_type_stats_.accepted_moves[proposed_action.logical_blk_type_index][(int)proposed_action.move_type]; + } + if (noc_opts_.noc){ + noc_cost_handler_->commit_noc_costs(); + costs_ += noc_delta_c; + } + + //Highlights the new block when manual move is selected. +#ifndef NO_GRAPHICS + if (manual_move_enabled) { + manual_move_highlight_new_block_location(); + } +#endif //NO_GRAPHICS + + } else { + VTR_ASSERT_SAFE(move_outcome == REJECTED); + + // Reset the net cost function flags first. + net_cost_handler_.reset_move_nets(); + + // Restore the blk_loc_registry.block_locs data structures to their state before the move. + blk_loc_registry.revert_move_blocks(blocks_affected_); + + if (place_algorithm == SLACK_TIMING_PLACE) { + /* Revert the timing delays and costs to pre-update values. */ + /* These routines must be called after reverting the block moves. */ + //TODO: make this process incremental + comp_td_connection_delays(delay_model_, placer_state_); + comp_td_costs(delay_model_, *criticalities_, placer_state_, &costs_.timing_cost); + + /* Re-invalidate the affected sink pins since the proposed + * move is rejected, and the same blocks are reverted to + * their original positions. */ + invalidate_affected_connections(blocks_affected_, pin_timing_invalidator_, timing_info_); + + // Revert the timing update + update_timing_classes(crit_params, timing_info_, criticalities_, + setup_slacks_, pin_timing_invalidator_, placer_state_); + + VTR_ASSERT_SAFE_MSG( + verify_connection_setup_slacks(setup_slacks_, placer_state_), + "The current setup slacks should be identical to the values before the try swap timing info update."); + } + + if (place_algorithm == CRITICALITY_TIMING_PLACE) { + // Un-stage the values stored in proposed_* data structures + revert_td_cost(blocks_affected_, placer_state_.mutable_timing()); + } + + if (proposed_action.logical_blk_type_index != -1) { //if the agent proposed the block type, then collect the block type stat + ++move_type_stats_.rejected_moves[proposed_action.logical_blk_type_index][(int)proposed_action.move_type]; + } + /* Revert the traffic flow routes within the NoC*/ + if (noc_opts_.noc) { + noc_cost_handler_->revert_noc_traffic_flow_routes(blocks_affected_); + } + } + + move_outcome_stats.delta_cost_norm = delta_c; + move_outcome_stats.delta_bb_cost_norm = bb_delta_c * costs_.bb_cost_norm; + move_outcome_stats.delta_timing_cost_norm = timing_delta_c * costs_.timing_cost_norm; + + move_outcome_stats.delta_bb_cost_abs = bb_delta_c; + move_outcome_stats.delta_timing_cost_abs = timing_delta_c; + + LOG_MOVE_STATS_OUTCOME(delta_c, bb_delta_c, timing_delta_c, (move_outcome ? "ACCEPTED" : "REJECTED"), ""); + } + move_outcome_stats.outcome = move_outcome; + + // If we force a router block move then it was not proposed by the + // move generator, so we should not calculate the reward and update + // the move generators status since this outcome is not a direct + // consequence of the move generator + if (!router_block_move) { + move_generator.calculate_reward_and_process_outcome(move_outcome_stats, delta_c, timing_bb_factor); + } + +#ifdef VTR_ENABLE_DEBUG_LOGGING +# ifndef NO_GRAPHICS + stop_placement_and_check_breakpoints(blocks_affected, move_outcome, delta_c, bb_delta_c, timing_delta_c); +# endif +#endif + + // Clear the data structure containing block move info + blocks_affected_.clear_move_blocks(); + +#if 0 + // Check that each accepted swap yields a valid placement. This will + // greatly slow the placer, but can debug some issues. + check_place(*costs, delay_model, criticalities, place_algorithm, noc_opts); +#endif + VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, + "\t\tAfter move Place cost %e, bb_cost %e, timing cost %e\n", + costs_.cost, costs_.bb_cost, costs_.timing_cost); + return move_outcome; +} + +/* Function to update the setup slacks and criticalities before the inner loop of the annealing/quench */ +void PlacementAnnealer::outer_loop_update_timing_info(int num_connections) { + if (placer_opts_.place_algorithm.is_timing_driven()) { + /* At each temperature change we update these values to be used + * for normalizing the tradeoff between timing and wirelength (bb) */ + if (outer_crit_iter_count_ >= placer_opts_.recompute_crit_iter + || placer_opts_.inner_loop_recompute_divider != 0) { +#ifdef VERBOSE + VTR_LOG("Outer loop recompute criticalities\n"); +#endif + // Avoid division by zero + num_connections = std::max(num_connections, 1); + VTR_ASSERT(num_connections > 0); + + PlaceCritParams crit_params; + crit_params.crit_exponent = annealing_state_.crit_exponent; + crit_params.crit_limit = placer_opts_.place_crit_limit; + + //Update all timing related classes + perform_full_timing_update(crit_params, delay_model_, criticalities_, setup_slacks_, + pin_timing_invalidator_, timing_info_, &costs_, placer_state_); + + outer_crit_iter_count_ = 0; + } + outer_crit_iter_count_++; + } + + // Update the cost normalization factors + update_placement_cost_normalization_factors(&costs_, placer_opts_, noc_opts_, noc_cost_handler_); +} + +/* Function which contains the inner loop of the simulated annealing */ +void placement_inner_loop(int inner_recompute_limit, + t_placer_statistics* stats, + + int* moves_since_cost_recompute, + PlacerSetupSlacks* setup_slacks, + MoveGenerator& move_generator, + float timing_bb_factor + ) { + // How many times have we dumped placement to a file this temperature? + int inner_placement_save_count = 0; + + stats->reset(); + + bool manual_move_enabled = false; + + // Inner loop begins + for (int inner_iter = 0, inner_crit_iter_count = 1; inner_iter < state->move_lim; inner_iter++) { + e_move_result swap_result = try_swap(move_generator, + placer_opts, noc_opts, move_type_stat, place_algorithm, + timing_bb_factor, manual_move_enabled); + + if (swap_result == ACCEPTED) { + /* Move was accepted. Update statistics that are useful for the annealing schedule. */ + stats->single_swap_update(*costs); + swap_stats.num_swap_accepted++; + } else if (swap_result == ABORTED) { + swap_stats.num_swap_aborted++; + } else { // swap_result == REJECTED + swap_stats.num_swap_rejected++; + } + + if (place_algorithm.is_timing_driven()) { + /* Do we want to re-timing analyze the circuit to get updated slack and criticality values? + * We do this only once in a while, since it is expensive. + */ + if (inner_crit_iter_count >= inner_recompute_limit + && inner_iter != state->move_lim - 1) { /*on last iteration don't recompute */ + + inner_crit_iter_count = 0; +#ifdef VERBOSE + VTR_LOG("Inner loop recompute criticalities\n"); +#endif + + PlaceCritParams crit_params; + crit_params.crit_exponent = state->crit_exponent; + crit_params.crit_limit = placer_opts.place_crit_limit; + + //Update all timing related classes + perform_full_timing_update(crit_params, delay_model, criticalities, + setup_slacks, pin_timing_invalidator, + timing_info, costs, placer_state); + } + inner_crit_iter_count++; + } + + /* Lines below prevent too much round-off error from accumulating + * in the cost over many iterations (due to incremental updates). + * This round-off can lead to error checks failing because the cost + * is different from what you get when you recompute from scratch. + */ + ++(*moves_since_cost_recompute); + if (*moves_since_cost_recompute > MAX_MOVES_BEFORE_RECOMPUTE) { + net_cost_handler.recompute_costs_from_scratch(delay_model, criticalities, *costs); + + if (noc_cost_handler.has_value()) { + noc_cost_handler->recompute_costs_from_scratch(noc_opts, *costs); + } + + *moves_since_cost_recompute = 0; + } + + if (placer_opts.placement_saves_per_temperature >= 1 && inner_iter > 0 + && (inner_iter + 1) % (state->move_lim / placer_opts.placement_saves_per_temperature) == 0) { + std::string filename = vtr::string_fmt("placement_%03d_%03d.place", + state->num_temps + 1, inner_placement_save_count); + VTR_LOG("Saving placement to file at temperature move %d / %d: %s\n", + inner_iter, state->move_lim, filename.c_str()); + print_place(nullptr, nullptr, filename.c_str(), placer_state.block_locs()); + ++inner_placement_save_count; + } + } + + /* Calculate the success_rate and std_dev of the costs. */ + stats->calc_iteration_stats(*costs, state->move_lim); } \ No newline at end of file diff --git a/vpr/src/place/annealer.h b/vpr/src/place/annealer.h index 0c046bcdf5f..b19ef8f5968 100644 --- a/vpr/src/place/annealer.h +++ b/vpr/src/place/annealer.h @@ -1,9 +1,32 @@ #pragma once +#include "vpr_types.h" + +#include "move_generator.h" // movestats +#include "net_cost_handler.h" + +#include + +class PlacerState; class t_placer_costs; struct t_placer_opts; -struct t_annealing_sched; + +class NocCostHandler; +class ManualMoveGenerator; +class NetPinTimingInvalidator; + +/** + * These variables keep track of the number of swaps + * rejected, accepted or aborted. The total number of swap attempts + * is the sum of the three number. + */ +struct t_swap_stats { + int num_swap_rejected = 0; + int num_swap_accepted = 0; + int num_swap_aborted = 0; + int num_ts_called = 0; +}; /** * @brief Stores variables that are used by the annealing process. @@ -71,7 +94,7 @@ class t_annealing_state { float INVERSE_DELTA_RLIM; public: //Constructor - t_annealing_state() = delete; + t_annealing_state() = default; t_annealing_state(const t_annealing_sched& annealing_sched, float first_t, float first_rlim, @@ -128,8 +151,83 @@ class t_annealing_state { inline void update_move_lim(float success_target, float success_rate); }; + class PlacementAnnealer { + public: + PlacementAnnealer(const t_placer_opts& placer_opts, + PlacerState& placer_state, + t_placer_costs& costs, + NetCostHandler& net_cost_handler, + std::optional& noc_cost_handler, + const t_noc_opts& noc_opts, + MoveGenerator& move_generator_1, + MoveGenerator& move_generator_2, + ManualMoveGenerator& manual_move_generator, + const PlaceDelayModel* delay_model, + PlacerCriticalities* criticalities, + PlacerSetupSlacks* setup_slacks, + SetupTimingInfo* timing_info, + NetPinTimingInvalidator* pin_timing_invalidator, + int move_lim); + + void placement_inner_loop(const t_annealing_state* state, + const t_placer_opts& placer_opts, + const t_noc_opts& noc_opts, + int inner_recompute_limit, + t_placer_statistics* stats, + t_placer_costs* costs, + int* moves_since_cost_recompute, + NetPinTimingInvalidator* pin_timing_invalidator, + const PlaceDelayModel* delay_model, + PlacerCriticalities* criticalities, + PlacerSetupSlacks* setup_slacks, + MoveGenerator& move_generator, + ManualMoveGenerator& manual_move_generator, + t_pl_blocks_to_be_moved& blocks_affected, + SetupTimingInfo* timing_info, + const t_place_algorithm& place_algorithm, + MoveTypeStat& move_type_stat, + float timing_bb_factor, + t_swap_stats& swap_stats, + PlacerState& placer_state, + NetCostHandler& net_cost_handler, + std::optional& noc_cost_handler); + + void outer_loop_update_timing_info(int num_connections); + + e_move_result try_swap(MoveGenerator& move_generator, + const t_place_algorithm& place_algorithm, + float timing_bb_factor, + bool manual_move_enabled); + + public: + const t_placer_opts& placer_opts_; + PlacerState& placer_state_; + t_placer_costs& costs_; + NetCostHandler& net_cost_handler_; + std::optional& noc_cost_handler_; + const t_noc_opts& noc_opts_; + + MoveGenerator& move_generator_1_; + MoveGenerator& move_generator_2_; + ManualMoveGenerator& manual_move_generator_; + + const PlaceDelayModel* delay_model_; + PlacerCriticalities* criticalities_; + PlacerSetupSlacks* setup_slacks_; + SetupTimingInfo* timing_info_; + NetPinTimingInvalidator* pin_timing_invalidator_; + std::unique_ptr move_stats_file_; + int outer_crit_iter_count_; - private: t_annealing_state annealing_state_; + /// Swap statistics keep record of the number accepted/rejected/aborted swaps. + t_swap_stats swap_stats_; + MoveTypeStat move_type_stats_; + + t_pl_blocks_to_be_moved blocks_affected_; + + + private: + float estimate_starting_temperature(); }; \ No newline at end of file diff --git a/vpr/src/place/move_utils.h b/vpr/src/place/move_utils.h index 0c221f89c4a..99151695dab 100644 --- a/vpr/src/place/move_utils.h +++ b/vpr/src/place/move_utils.h @@ -91,18 +91,6 @@ struct t_range_limiters { float dm_rlim; }; -/** - * These variables keep track of the number of swaps - * rejected, accepted or aborted. The total number of swap attempts - * is the sum of the three number. - */ -struct t_swap_stats { - int num_swap_rejected = 0; - int num_swap_accepted = 0; - int num_swap_aborted = 0; - int num_ts_called = 0; -}; - e_create_move create_move(t_pl_blocks_to_be_moved& blocks_affected, ClusterBlockId b_from, t_pl_loc to, diff --git a/vpr/src/place/place.cpp b/vpr/src/place/place.cpp index 90b566fb753..33fc09fa342 100644 --- a/vpr/src/place/place.cpp +++ b/vpr/src/place/place.cpp @@ -90,84 +90,6 @@ static constexpr int MAX_MOVES_BEFORE_RECOMPUTE = 500000; constexpr float INVALID_DELAY = std::numeric_limits::quiet_NaN(); constexpr float INVALID_COST = std::numeric_limits::quiet_NaN(); -/********************** Variables local to place.c ***************************/ - - -std::unique_ptr f_move_stats_file(nullptr, - vtr::fclose); - -#ifdef VTR_ENABLE_DEBUG_LOGGIING -# define LOG_MOVE_STATS_HEADER() \ - do { \ - if (f_move_stats_file) { \ - fprintf(f_move_stats_file.get(), \ - "temp,from_blk,to_blk,from_type,to_type," \ - "blk_count," \ - "delta_cost,delta_bb_cost,delta_td_cost," \ - "outcome,reason\n"); \ - } \ - } while (false) - -# define LOG_MOVE_STATS_PROPOSED(t, affected_blocks) \ - do { \ - if (f_move_stats_file) { \ - auto& place_ctx = g_vpr_ctx.placement(); \ - auto& cluster_ctx = g_vpr_ctx.clustering(); \ - ClusterBlockId b_from = affected_blocks.moved_blocks[0].block_num; \ - \ - t_pl_loc to = affected_blocks.moved_blocks[0].new_loc; \ - ClusterBlockId b_to = place_ctx.grid_blocks[to.x][to.y].blocks[to.sub_tile]; \ - \ - t_logical_block_type_ptr from_type = cluster_ctx.clb_nlist.block_type(b_from); \ - t_logical_block_type_ptr to_type = nullptr; \ - if (b_to) { \ - to_type = cluster_ctx.clb_nlist.block_type(b_to); \ - } \ - \ - fprintf(f_move_stats_file.get(), \ - "%g," \ - "%d,%d," \ - "%s,%s," \ - "%d,", \ - t, \ - int(b_from), int(b_to), \ - from_type->name, (to_type ? to_type->name : "EMPTY"), \ - affected_blocks.moved_blocks.size()); \ - } \ - } while (false) - -# define LOG_MOVE_STATS_OUTCOME(delta_cost, delta_bb_cost, delta_td_cost, \ - outcome, reason) \ - do { \ - if (f_move_stats_file) { \ - fprintf(f_move_stats_file.get(), \ - "%g,%g,%g," \ - "%s,%s\n", \ - delta_cost, delta_bb_cost, delta_td_cost, \ - outcome, reason); \ - } \ - } while (false) - -#else - -# define LOG_MOVE_STATS_HEADER() \ - do { \ - fprintf(f_move_stats_file.get(), \ - "VTR_ENABLE_DEBUG_LOGGING disabled " \ - "-- No move stats recorded\n"); \ - } while (false) - -# define LOG_MOVE_STATS_PROPOSED(t, blocks_affected) \ - do { \ - } while (false) - -# define LOG_MOVE_STATS_OUTCOME(delta_cost, delta_bb_cost, delta_td_cost, \ - outcome, reason) \ - do { \ - } while (false) - -#endif - /********************* Static subroutines local to place.c *******************/ #ifdef VERBOSE void print_clb_placement(const char* fname); @@ -254,43 +176,9 @@ static float starting_t(const t_annealing_state* state, static int count_connections(); -static void commit_td_cost(const t_pl_blocks_to_be_moved& blocks_affected, - PlacerState& placer_state); - -static void revert_td_cost(const t_pl_blocks_to_be_moved& blocks_affected, - PlacerTimingContext& p_timing_ctx); - -static void invalidate_affected_connections( - const t_pl_blocks_to_be_moved& blocks_affected, - NetPinTimingInvalidator* pin_tedges_invalidator, - TimingInfo* timing_info); - static float analyze_setup_slack_cost(const PlacerSetupSlacks* setup_slacks, const PlacerState& placer_state); -static e_move_result assess_swap(double delta_c, double t); - -static void update_placement_cost_normalization_factors(t_placer_costs* costs, - const t_placer_opts& placer_opts, - const t_noc_opts& noc_opts, - const std::optional& noc_cost_handler); - -static double get_total_cost(t_placer_costs* costs, const t_placer_opts& placer_opts, const t_noc_opts& noc_opts); - -static void outer_loop_update_timing_info(const t_placer_opts& placer_opts, - const t_noc_opts& noc_opts, - t_placer_costs* costs, - int num_connections, - float crit_exponent, - int* outer_crit_iter_count, - const PlaceDelayModel* delay_model, - PlacerCriticalities* criticalities, - PlacerSetupSlacks* setup_slacks, - NetPinTimingInvalidator* pin_timing_invalidator, - SetupTimingInfo* timing_info, - PlacerState& placer_state, - const std::optional& noc_cost_handler); - static void placement_inner_loop(const t_annealing_state* state, const t_placer_opts& placer_opts, const t_noc_opts& noc_opts, @@ -346,7 +234,6 @@ static void copy_locs_to_global_state(const BlkLocRegistry& blk_loc_registry); /*****************************************************************************/ void try_place(const Netlist<>& net_list, const t_placer_opts& placer_opts, - t_annealing_sched annealing_sched, const t_router_opts& router_opts, const t_analysis_opts& analysis_opts, const t_noc_opts& noc_opts, @@ -372,8 +259,6 @@ void try_place(const Netlist<>& net_list, auto& timing_ctx = g_vpr_ctx.timing(); auto pre_place_timing_stats = timing_ctx.stats; int tot_iter, moves_since_cost_recompute, num_connections, outer_crit_iter_count; - float first_crit_exponent; - t_placer_costs costs(placer_opts.place_algorithm); @@ -395,9 +280,6 @@ void try_place(const Netlist<>& net_list, t_pl_blocks_to_be_moved blocks_affected(net_list.blocks().size()); - // Swap statistics keep record of the number accepted/rejected/aborted swaps. - t_swap_stats swap_stats; - if (placer_opts.place_algorithm.is_timing_driven()) { /*do this before the initial placement to avoid messing up the initial placement */ place_delay_model = alloc_lookups_and_delay_model(net_list, @@ -422,7 +304,7 @@ void try_place(const Netlist<>& net_list, VTR_LOG("Bounding box mode is %s\n", (cube_bb ? "Cube" : "Per-layer")); VTR_LOG("\n"); - int move_lim = (int)(annealing_sched.inner_num * pow(net_list.blocks().size(), 1.3333)); + int move_lim = (int)(placer_opts.anneal_sched.inner_num * pow(net_list.blocks().size(), 1.3333)); PlacerState placer_state; auto& place_move_ctx = placer_state.mutable_move(); @@ -490,8 +372,6 @@ void try_place(const Netlist<>& net_list, if (placer_opts.place_algorithm.is_timing_driven()) { costs.bb_cost = net_cost_handler.comp_bb_cost(e_cost_methods::NORMAL); - first_crit_exponent = placer_opts.td_place_exp_first; /*this will be modified when rlim starts to change */ - num_connections = count_connections(); VTR_LOG("\n"); VTR_LOG("There are %d point to point connections in this circuit.\n", @@ -530,7 +410,7 @@ void try_place(const Netlist<>& net_list, //First time compute timing and costs, compute from scratch PlaceCritParams crit_params; - crit_params.crit_exponent = first_crit_exponent; + crit_params.crit_exponent = placer_opts.td_place_exp_first; crit_params.crit_limit = placer_opts.place_crit_limit; initialize_timing_info(crit_params, place_delay_model.get(), placer_criticalities.get(), @@ -574,7 +454,6 @@ void try_place(const Netlist<>& net_list, /* Other initializations */ outer_crit_iter_count = 0; num_connections = 0; - first_crit_exponent = 0; } if (noc_opts.noc) { @@ -650,62 +529,6 @@ void try_place(const Netlist<>& net_list, print_place(nullptr, nullptr, filename.c_str(), blk_loc_registry.block_locs()); } - int first_move_lim = get_initial_move_lim(placer_opts, annealing_sched); - - int inner_recompute_limit; - if (placer_opts.inner_loop_recompute_divider != 0) { - inner_recompute_limit = static_cast(0.5 + (float)first_move_lim / (float)placer_opts.inner_loop_recompute_divider); - } else { - /*don't do an inner recompute */ - inner_recompute_limit = first_move_lim + 1; - } - - /* calculate the number of moves in the quench that we should recompute timing after based on the value of * - * the commandline option quench_recompute_divider */ - int quench_recompute_limit; - if (placer_opts.quench_recompute_divider != 0) { - quench_recompute_limit = static_cast(0.5 + (float)move_lim / (float)placer_opts.quench_recompute_divider); - } else { - /*don't do an quench recompute */ - quench_recompute_limit = first_move_lim + 1; - } - - //allocate helper vectors that are used by many move generators - place_move_ctx.X_coord.resize(10, 0); - place_move_ctx.Y_coord.resize(10, 0); - place_move_ctx.layer_coord.resize(10, 0); - - //allocate move type statistics vectors - MoveTypeStat move_type_stat; - move_type_stat.blk_type_moves.resize({device_ctx.logical_block_types.size(), (int)e_move_type::NUMBER_OF_AUTO_MOVES}, 0); - move_type_stat.accepted_moves.resize({device_ctx.logical_block_types.size(), (int)e_move_type::NUMBER_OF_AUTO_MOVES}, 0); - move_type_stat.rejected_moves.resize({device_ctx.logical_block_types.size(), (int)e_move_type::NUMBER_OF_AUTO_MOVES}, 0); - - /* Get the first range limiter */ - float first_rlim = (float)std::max(device_ctx.grid.width() - 1, device_ctx.grid.height() - 1); - place_move_ctx.first_rlim = first_rlim; - - t_annealing_state state(annealing_sched, - EPSILON, // Set the temperature low to ensure that initial placement quality will be preserved - first_rlim, - first_move_lim, - first_crit_exponent); - - /* Update the starting temperature for placement annealing to a more appropriate value */ - state.t = starting_t(&state, &costs, annealing_sched, - place_delay_model.get(), placer_criticalities.get(), - placer_setup_slacks.get(), timing_info.get(), *move_generator, - manual_move_generator, pin_timing_invalidator.get(), - blocks_affected, placer_opts, noc_opts, move_type_stat, - swap_stats, placer_state, net_cost_handler, noc_cost_handler); - - if (!placer_opts.move_stats_file.empty()) { - f_move_stats_file = std::unique_ptr( - vtr::fopen(placer_opts.move_stats_file.c_str(), "w"), - vtr::fclose); - LOG_MOVE_STATS_HEADER(); - } - tot_iter = 0; moves_since_cost_recompute = 0; @@ -727,6 +550,10 @@ void try_place(const Netlist<>& net_list, //Define the timing bb weight factor for the agent's reward function float timing_bb_factor = REWARD_BB_TIMING_RELATIVE_WEIGHT; + PlacementAnnealer annealer(placer_opts, placer_state, costs, net_cost_handler, noc_cost_handler, + noc_opts, *move_generator, *move_generator2, manual_move_generator, place_delay_model.get(), + placer_criticalities.get(), placer_setup_slacks.get(), timing_info.get(), pin_timing_invalidator.get(), move_lim); + if (!skip_anneal) { //Table header VTR_LOG("\n"); @@ -736,21 +563,15 @@ void try_place(const Netlist<>& net_list, do { vtr::Timer temperature_timer; - outer_loop_update_timing_info(placer_opts, noc_opts, &costs, num_connections, - state.crit_exponent, &outer_crit_iter_count, - place_delay_model.get(), placer_criticalities.get(), - placer_setup_slacks.get(), pin_timing_invalidator.get(), - timing_info.get(), placer_state, noc_cost_handler); + annealer.outer_loop_update_timing_info(num_connections); if (placer_opts.place_algorithm.is_timing_driven()) { critical_path = timing_info->least_slack_critical_path(); sTNS = timing_info->setup_total_negative_slack(); sWNS = timing_info->setup_worst_negative_slack(); - //see if we should save the current placement solution as a checkpoint - - if (placer_opts.place_checkpointing - && agent_state == e_agent_state::LATE_IN_THE_ANNEAL) { + // see if we should save the current placement solution as a checkpoint + if (placer_opts.place_checkpointing && agent_state == e_agent_state::LATE_IN_THE_ANNEAL) { save_placement_checkpoint_if_needed(blk_loc_registry.block_locs(), placement_checkpoint, timing_info, costs, critical_path.delay()); @@ -761,7 +582,7 @@ void try_place(const Netlist<>& net_list, assign_current_move_generator(move_generator, move_generator2, agent_state, placer_opts, false, current_move_generator); - //do a complete inner loop iteration + // do a complete inner loop iteration placement_inner_loop(&state, placer_opts, noc_opts, inner_recompute_limit, &stats, &costs, &moves_since_cost_recompute, @@ -818,11 +639,7 @@ void try_place(const Netlist<>& net_list, vtr::ScopedFinishTimer temperature_timer("Placement Quench"); - outer_loop_update_timing_info(placer_opts, noc_opts, &costs, num_connections, - state.crit_exponent, &outer_crit_iter_count, - place_delay_model.get(), placer_criticalities.get(), - placer_setup_slacks.get(), pin_timing_invalidator.get(), - timing_info.get(), placer_state, noc_cost_handler); + annealer.outer_loop_update_timing_info(num_connections); //move the appropriate move_generator to be the current used move generator assign_current_move_generator(move_generator, move_generator2, @@ -991,47 +808,7 @@ void try_place(const Netlist<>& net_list, copy_locs_to_global_state(blk_loc_registry); } -/* Function to update the setup slacks and criticalities before the inner loop of the annealing/quench */ -static void outer_loop_update_timing_info(const t_placer_opts& placer_opts, - const t_noc_opts& noc_opts, - t_placer_costs* costs, - int num_connections, - float crit_exponent, - int* outer_crit_iter_count, - const PlaceDelayModel* delay_model, - PlacerCriticalities* criticalities, - PlacerSetupSlacks* setup_slacks, - NetPinTimingInvalidator* pin_timing_invalidator, - SetupTimingInfo* timing_info, - PlacerState& placer_state, - const std::optional& noc_cost_handler) { - if (placer_opts.place_algorithm.is_timing_driven()) { - /*at each temperature change we update these values to be used */ - /*for normalizing the tradeoff between timing and wirelength (bb) */ - if (*outer_crit_iter_count >= placer_opts.recompute_crit_iter - || placer_opts.inner_loop_recompute_divider != 0) { -#ifdef VERBOSE - VTR_LOG("Outer loop recompute criticalities\n"); -#endif - num_connections = std::max(num_connections, 1); //Avoid division by zero - VTR_ASSERT(num_connections > 0); - - PlaceCritParams crit_params; - crit_params.crit_exponent = crit_exponent; - crit_params.crit_limit = placer_opts.place_crit_limit; - //Update all timing related classes - perform_full_timing_update(crit_params, delay_model, criticalities, setup_slacks, - pin_timing_invalidator, timing_info, costs, placer_state); - - *outer_crit_iter_count = 0; - } - (*outer_crit_iter_count)++; - } - - /* Update the cost normalization factors */ - update_placement_cost_normalization_factors(costs, placer_opts, noc_opts, noc_cost_handler); -} /* Function which contains the inner loop of the simulated annealing */ static void placement_inner_loop(const t_annealing_state* state, @@ -1618,223 +1395,6 @@ static bool is_cube_bb(const e_place_bounding_box_mode place_bb_mode, return cube_bb; } -/** - * @brief Updates all the cost normalization factors during the outer - * loop iteration of the placement. At each temperature change, these - * values are updated so that we can balance the tradeoff between the - * different placement cost components (timing, wirelength and NoC). - * Depending on the placement mode the corresponding normalization factors are - * updated. - * - * @param costs Contains the normalization factors which need to be updated - * @param placer_opts Determines the placement mode - * @param noc_opts Determines if placement includes the NoC - * @param noc_cost_handler Computes normalization factors for NoC-related cost terms - */ -static void update_placement_cost_normalization_factors(t_placer_costs* costs, - const t_placer_opts& placer_opts, - const t_noc_opts& noc_opts, - const std::optional& noc_cost_handler) { - /* Update the cost normalization factors */ - costs->update_norm_factors(); - - // update the noc normalization factors if the placement includes the NoC - if (noc_opts.noc) { - noc_cost_handler->update_noc_normalization_factors(*costs); - } - - // update the current total placement cost - costs->cost = get_total_cost(costs, placer_opts, noc_opts); -} - -/** - * @brief Compute the total normalized cost for a given placement. This - * computation will vary depending on the placement modes. - * - * @param costs The current placement cost components and their normalization - * factors - * @param placer_opts Determines the placement mode - * @param noc_opts Determines if placement includes the NoC - * @return double The computed total cost of the current placement - */ -static double get_total_cost(t_placer_costs* costs, const t_placer_opts& placer_opts, const t_noc_opts& noc_opts) { - double total_cost = 0.0; - - if (placer_opts.place_algorithm == BOUNDING_BOX_PLACE) { - // in bounding box mode we only care about wirelength - total_cost = costs->bb_cost * costs->bb_cost_norm; - } else if (placer_opts.place_algorithm.is_timing_driven()) { - // in timing mode we include both wirelength and timing costs - total_cost = (1 - placer_opts.timing_tradeoff) * (costs->bb_cost * costs->bb_cost_norm) + (placer_opts.timing_tradeoff) * (costs->timing_cost * costs->timing_cost_norm); - } - - if (noc_opts.noc) { - // in noc mode we include noc aggregate bandwidth and noc latency - total_cost += calculate_noc_cost(costs->noc_cost_terms, costs->noc_cost_norm_factors, noc_opts); - } - - return total_cost; -} - -/** - * @brief Check if the setup slack has gotten better or worse due to block swap. - * - * Get all the modified slack values via the PlacerSetupSlacks class, and compare - * then with the original values at these connections. Sort them and compare them - * one by one, and return the difference of the first different pair. - * - * If the new slack value is larger(better), than return a negative value so that - * the move will be accepted. If the new slack value is smaller(worse), return a - * positive value so that the move will be rejected. - * - * If no slack values have changed, then return an arbitrary positive number. A - * move resulting in no change in the slack values should probably be unnecessary. - * - * The sorting is need to prevent in the unlikely circumstances that a bad slack - * value suddenly got very good due to the block move, while a good slack value - * got very bad, perhaps even worse than the original worse slack value. - */ -static float analyze_setup_slack_cost(const PlacerSetupSlacks* setup_slacks, - const PlacerState& placer_state) { - const auto& cluster_ctx = g_vpr_ctx.clustering(); - const auto& clb_nlist = cluster_ctx.clb_nlist; - - const auto& p_timing_ctx = placer_state.timing(); - const auto& connection_setup_slack = p_timing_ctx.connection_setup_slack; - - //Find the original/proposed setup slacks of pins with modified values - std::vector original_setup_slacks, proposed_setup_slacks; - - auto clb_pins_modified = setup_slacks->pins_with_modified_setup_slack(); - for (ClusterPinId clb_pin : clb_pins_modified) { - ClusterNetId net_id = clb_nlist.pin_net(clb_pin); - size_t ipin = clb_nlist.pin_net_index(clb_pin); - - original_setup_slacks.push_back(connection_setup_slack[net_id][ipin]); - proposed_setup_slacks.push_back( - setup_slacks->setup_slack(net_id, ipin)); - } - - //Sort in ascending order, from the worse slack value to the best - std::stable_sort(original_setup_slacks.begin(), original_setup_slacks.end()); - std::stable_sort(proposed_setup_slacks.begin(), proposed_setup_slacks.end()); - - //Check the first pair of slack values that are different - //If found, return their difference - for (size_t idiff = 0; idiff < original_setup_slacks.size(); ++idiff) { - float slack_diff = original_setup_slacks[idiff] - - proposed_setup_slacks[idiff]; - - if (slack_diff != 0) { - return slack_diff; - } - } - - //If all slack values are identical (or no modified slack values), - //reject this move by returning an arbitrary positive number as cost. - return 1; -} - -static e_move_result assess_swap(double delta_c, double t) { - /* Returns: 1 -> move accepted, 0 -> rejected. */ - VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\tTemperature is: %e delta_c is %e\n", t, delta_c); - if (delta_c <= 0) { - VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is accepted(delta_c < 0)\n"); - return ACCEPTED; - } - - if (t == 0.) { - VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is rejected(t == 0)\n"); - return REJECTED; - } - - float fnum = vtr::frand(); - float prob_fac = std::exp(-delta_c / t); - if (prob_fac > fnum) { - VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is accepted(hill climbing)\n"); - return ACCEPTED; - } - VTR_LOGV_DEBUG(g_vpr_ctx.placement().f_placer_debug, "\t\tMove is rejected(hill climbing)\n"); - return REJECTED; -} - -/** - * @brief Update the connection_timing_cost values from the temporary - * values for all connections that have/haven't changed. - * - * All the connections have already been gathered by blocks_affected.affected_pins - * after running the routine find_affected_nets_and_update_costs() in try_swap(). - */ -static void commit_td_cost(const t_pl_blocks_to_be_moved& blocks_affected, - PlacerState& placer_state) { - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& clb_nlist = cluster_ctx.clb_nlist; - - auto& p_timing_ctx = placer_state.mutable_timing(); - auto& connection_delay = p_timing_ctx.connection_delay; - auto& proposed_connection_delay = p_timing_ctx.proposed_connection_delay; - auto& connection_timing_cost = p_timing_ctx.connection_timing_cost; - auto& proposed_connection_timing_cost = p_timing_ctx.proposed_connection_timing_cost; - - //Go through all the sink pins affected - for (ClusterPinId pin_id : blocks_affected.affected_pins) { - ClusterNetId net_id = clb_nlist.pin_net(pin_id); - int ipin = clb_nlist.pin_net_index(pin_id); - - //Commit the timing delay and cost values - connection_delay[net_id][ipin] = proposed_connection_delay[net_id][ipin]; - proposed_connection_delay[net_id][ipin] = INVALID_DELAY; - connection_timing_cost[net_id][ipin] = proposed_connection_timing_cost[net_id][ipin]; - proposed_connection_timing_cost[net_id][ipin] = INVALID_DELAY; - } -} - -//Reverts modifications to proposed_connection_delay and proposed_connection_timing_cost based on -//the move proposed in blocks_affected -static void revert_td_cost(const t_pl_blocks_to_be_moved& blocks_affected, - PlacerTimingContext& p_timing_ctx) { -#ifndef VTR_ASSERT_SAFE_ENABLED - (void)blocks_affected; - (void)p_timing_ctx; -#else - //Invalidate temp delay & timing cost values to match sanity checks in - //comp_td_connection_cost() - auto& cluster_ctx = g_vpr_ctx.clustering(); - auto& clb_nlist = cluster_ctx.clb_nlist; - - auto& proposed_connection_delay = p_timing_ctx.proposed_connection_delay; - auto& proposed_connection_timing_cost = p_timing_ctx.proposed_connection_timing_cost; - - for (ClusterPinId pin : blocks_affected.affected_pins) { - ClusterNetId net = clb_nlist.pin_net(pin); - int ipin = clb_nlist.pin_net_index(pin); - proposed_connection_delay[net][ipin] = INVALID_DELAY; - proposed_connection_timing_cost[net][ipin] = INVALID_DELAY; - } -#endif -} - -/** - * @brief Invalidates the connections affected by the specified block moves. - * - * All the connections recorded in blocks_affected.affected_pins have different - * values for `proposed_connection_delay` and `connection_delay`. - * - * Invalidate all the timing graph edges associated with these connections via - * the NetPinTimingInvalidator class. - */ -static void invalidate_affected_connections(const t_pl_blocks_to_be_moved& blocks_affected, - NetPinTimingInvalidator* pin_tedges_invalidator, - TimingInfo* timing_info) { - VTR_ASSERT_SAFE(timing_info); - VTR_ASSERT_SAFE(pin_tedges_invalidator); - - /* Invalidate timing graph edges affected by the move */ - for (ClusterPinId pin : blocks_affected.affected_pins) { - pin_tedges_invalidator->invalidate_connection(pin, timing_info); - } -} - /* Allocates the major structures needed only by the placer, primarily for * * computing costs quickly and such. */ static NetCostHandler alloc_and_load_placement_structs(const t_placer_opts& placer_opts, diff --git a/vpr/src/place/place.h b/vpr/src/place/place.h index 138c6cdd05d..210663823a8 100644 --- a/vpr/src/place/place.h +++ b/vpr/src/place/place.h @@ -5,7 +5,6 @@ void try_place(const Netlist<>& net_list, const t_placer_opts& placer_opts, - t_annealing_sched annealing_sched, const t_router_opts& router_opts, const t_analysis_opts& analysis_opts, const t_noc_opts& noc_opts, diff --git a/vpr/src/place/placer_state.h b/vpr/src/place/placer_state.h index 97941f639b1..344839a1bd5 100644 --- a/vpr/src/place/placer_state.h +++ b/vpr/src/place/placer_state.h @@ -119,6 +119,14 @@ struct PlacerMoveContext : public Context { // Container to save the highly critical pins (higher than a timing criticality limit set by commandline option) std::vector> highly_crit_pins; + + public: + PlacerMoveContext() { + // allocate helper vectors that are used by many move generators + X_coord.resize(10, 0); + Y_coord.resize(10, 0); + layer_coord.resize(10, 0); + } }; /** From 9d824faecb0f2371eafe493c80ebba0c45cd003c Mon Sep 17 00:00:00 2001 From: ZohairZaidi Date: Wed, 16 Oct 2024 00:01:17 -0400 Subject: [PATCH 029/162] updated quickstart pics and added ODIN II --- doc/src/quickstart/index.rst | 74 ++++++++++++++++++++++++++---- doc/src/quickstart/tseng_blk1.png | Bin 70854 -> 48761 bytes doc/src/quickstart/tseng_nets.png | Bin 76728 -> 79660 bytes 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/doc/src/quickstart/index.rst b/doc/src/quickstart/index.rst index 4379745abd2..7762e0aab2d 100644 --- a/doc/src/quickstart/index.rst +++ b/doc/src/quickstart/index.rst @@ -2,7 +2,7 @@ VTR Quick Start ############### -This is a quick introduction to VTR which covers how to run VTR and some of its associated tools (:ref:`VPR`, :ref:`odin_ii`, :ref:`ABC`). +This is a quick introduction to VTR which covers how to run VTR and some of its associated tools (:ref:`VPR`, :ref:`Parmys`, :ref:`ABC`). Setting Up VTR ============== @@ -48,14 +48,6 @@ On most unix-like systems you can run: > make -The default front-end for VTR is :ref:`Parmys`, but you can build with ODIN II instead using the command below. This is required to run :ref:`Synthesizing with ODIN II`. - -.. code-block:: bash - - > make CMAKE_PARAMS="-DWITH_ODIN=on" - -from the VTR root directory (hereafter referred to as :term:`$VTR_ROOT`) to build VTR. - .. note:: In the VTR documentation lines starting with ``>`` (like ``> make`` above), indicate a command (i.e. ``make``) to run from your terminal. @@ -198,7 +190,7 @@ As an exercise try the following: .. figure:: tseng_blk1.png - Input (blue)/output (red) nets of block ``n_n3199`` (highlighted green). + Input (blue)/output (red) nets of block ``n_n3226`` (highlighted green). .. note:: If you do not provide :option:`--analysis `, VPR will re-implement the circuit from scratch. @@ -466,6 +458,68 @@ which we can visualize with: --route_chan_width 100 \ --analysis --disp on +Manually Running VTR with ODIN II +---------------------------------- +Let's start by making a new directory for us to work in: + +.. code-block:: bash + + > mkdir -p ~/vtr_work/quickstart/blink_manual + > cd ~/vtr_work/quickstart/blink_manual + +To synthesize your Verilog design with ODIN II, you need to build VTR with the following command: + +.. code-block:: bash + + > make CMAKE_PARAMS="-DWITH_ODIN=on" + +This step enables ODIN II support in the VTR build process, which is essential for the subsequent synthesis operations. + + +Next, run ODIN II on your Verilog file to synthesize it into a circuit netlist. Use the command below, specifying the required options: + + * ``-a $VTR_ROOT/vtr_flow/arch/timing/EArch.xml`` which specifies what FPGA architecture we are targeting, + * ``-V $VTR_ROOT/doc/src/quickstart/blink.v`` which specifies the verilog file we want to synthesize, and + * ``-o blink.odin.blif`` which specifies the name of the generated ``.blif`` circuit netlist. + +The resulting command is: + +.. code-block:: bash + + > $VTR_ROOT/odin_ii/odin_ii \ + -a $VTR_ROOT/vtr_flow/arch/timing/EArch.xml \ + -V $VTR_ROOT/doc/src/quickstart/blink.v \ + -o blink.odin.blif + +After running the command, you should see an output similar to the following:: + + Total time: 14.7ms + Odin ran with exit status: 0 + Odin II took 0.01 seconds (max_rss 5.1 MiB) + +where ``Odin ran with exit status: 0`` indicates Odin successfully synthesized our verilog. + +We can now take a look at the circuit which ODIN produced (``blink.odin.blif``). +The file is long and likely harder to follow than our code in ``blink.v``; however it implements the same functionality. +Some interesting highlights are shown below: + +.. literalinclude:: blink.odin.blif + :lines: 14,40 + :caption: Instantiations of rising-edge triggered Latches (i.e. Flip-Flops) in ``blink.odin.blif`` (implements part of ``r_counter`` in blink.v) + +.. literalinclude:: blink.odin.blif + :lines: 17-19,21-22 + :caption: Adder primitive instantiations in ``blink.odin.blif``, used to perform addition (implements part of the ``+`` operator in blink.v) + +.. literalinclude:: blink.odin.blif + :lines: 45-50 + :caption: Logic equation (.names truth-table) in ``blink.odin.blif``, implementing logical OR (implements part of the ``<`` operator in blink.v) + +.. seealso:: For more information on the BLIF file format see :ref:`blif_format`. + + +After synthesizing the netlist, you can proceed to follow the steps outlined in the section titled :ref:'Optimizing and Technology Mapping with ABC' using the generated `blink.odin.blif` file instead of `blink.parmys.blif`. + Next Steps ========== Now that you've finished the VTR quickstart, you're ready to start experimenting and using VTR. diff --git a/doc/src/quickstart/tseng_blk1.png b/doc/src/quickstart/tseng_blk1.png index 20870a5da13604bbc2618f94654f0197d544dda1..63ab4714b6139928e5fd68c650f59f71ddc1eb04 100644 GIT binary patch literal 48761 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 E0O><;LI3~& diff --git a/doc/src/quickstart/tseng_nets.png b/doc/src/quickstart/tseng_nets.png index e1608190744523340ac269c6cd2c889c941f0c8f..4be86d716d69145b459a820e7e8dedd357d2c6eb 100644 GIT binary patch literal 79660 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 V?AY`R$+xT$UG4jKif=nU{cozyUHkw5 literal 76728 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

}{b~ei zu>#(oTDM5gx-z93s=HgC#bYKqi!E8FCSf96p;l-HejI(xCy05IeQZ_6N>=CHkGw?TOn!m>|oOU2sm=I6t@j9X};rg)Q>3m zu;5e(*M9bQB`4(x;K|S4^`@GW_j$we(Mali$)HcNaLAwj^~n|9UELg5X4;dy4`Olw6;e3_isKTw6z$j$qu(e~cU9e+F*4|EyH zZeV(kgqIhoBy&y>>jm#hQE(B`KF(9Pc=aoip3!S7Df$WdL@!FUo}=SgxVa=R5mS$6+&V7H;uaom+A^S!R>>D;Ol;d%BMFS%kZXvkhm zYDYVF*-X8AhJ}e7#@E5qz`y{tt%;En0T%aFke8oXM>5r`XjcgA=}4yT#tHQ7((z!> z2A1tSTza;YG|CT%h4Kgk&=Y5pY&Y@~DTscUp5;f|Oe2%U zv=EPn>+!WkMQS1wQ;A~C-);bXNl#ZE4gO;oK(AXHcDuOeqN``, where ``

^l!8 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 CGi)pX From 3a700ce8b4062e5626d2f8ba50b3446fec857442 Mon Sep 17 00:00:00 2001 From: ZohairZaidi Date: Wed, 16 Oct 2024 00:28:17 -0400 Subject: [PATCH 030/162] updated automatically running vtr flow --- doc/src/quickstart/index.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/src/quickstart/index.rst b/doc/src/quickstart/index.rst index 7762e0aab2d..3f8f7c6a8a7 100644 --- a/doc/src/quickstart/index.rst +++ b/doc/src/quickstart/index.rst @@ -413,16 +413,16 @@ which should produce output similar to:: EArch/blink OK (took 0.26 seconds) -There are also multiple log files (including for ABC, ODIN and VPR), which by convention the script names with the ``.out`` suffix: +There are also multiple log files (including for ABC, Parmys and VPR), which by convention the script names with the ``.out`` suffix: .. code-block:: bash > ls *.out - 0_blackboxing_latch.out odin.out report_clocks.abc.out vanilla_restore_clocks.out + 0_blackboxing_latch.out parmys.out report_clocks.abc.out vanilla_restore_clocks.out abc0.out report_clk.out restore_latch0.out vpr.out -With the main log files of interest including the ODIN log file (``odin.out``), log files produced by ABC (e.g. ``abc0.out``), and the VPR log file (``vpr.out``). +With the main log files of interest including the Parmys log file (``parmys.out``), log files produced by ABC (e.g. ``abc0.out``), and the VPR log file (``vpr.out``). .. note:: @@ -435,10 +435,10 @@ You will also see there are several BLIF files produced: > ls *.blif - 0_blink.abc.blif 0_blink.raw.abc.blif blink.odin.blif - 0_blink.odin.blif blink.abc.blif blink.pre-vpr.blif + 0_blink.abc.blif 0_blink.raw.abc.blif blink.parmys.blif + 0_blink.parmys.blif blink.abc.blif blink.pre-vpr.blif -With the main files of interest being ``blink.odin.blif`` (netlist produced by ODIN), ``blink.abc.blif`` (final netlist produced by ABC after clock restoration), ``blink.pre-vpr.blif`` netlist used by VPR (usually identical to ``blink.abc.blif``). +With the main files of interest being ``blink.parmys.blif`` (netlist produced by Parmys), ``blink.abc.blif`` (final netlist produced by ABC after clock restoration), ``blink.pre-vpr.blif`` netlist used by VPR (usually identical to ``blink.abc.blif``). Like before, we can also see the implementation files generated by VPR: From 22ff4ee64c1e2c68ccf68ce6618c15dc5215e6fa Mon Sep 17 00:00:00 2001 From: ZohairZaidi Date: Thu, 17 Oct 2024 15:53:30 -0400 Subject: [PATCH 031/162] removed ABC from parmys section --- doc/src/quickstart/blink_implementation.png | Bin 61307 -> 25551 bytes doc/src/quickstart/index.rst | 144 ++++++++++---------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/doc/src/quickstart/blink_implementation.png b/doc/src/quickstart/blink_implementation.png index 8acb5085f68206240b562ca23d420544ef260c93..4b939c03cfb72bc3cc48a574a37af037ed599deb 100644 GIT binary patch literal 25551 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 literal 61307 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