From ccc2fdb37fba02228039d05f94c3e5707693faf5 Mon Sep 17 00:00:00 2001 From: Peter Heywood Date: Fri, 12 Jun 2020 19:35:26 +0100 Subject: [PATCH] Add early implementation of the flamegpu 1 stable marriage/stable matching example It doesn't always resolve all cases, even with > 1024 iterations, although neither does flamegpu1. --- CMakeLists.txt | 5 + examples/stable_matching/CMakeLists.txt | 40 ++++ examples/stable_matching/src/main.cu | 302 ++++++++++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 examples/stable_matching/CMakeLists.txt create mode 100644 examples/stable_matching/src/main.cu diff --git a/CMakeLists.txt b/CMakeLists.txt index c98a4b413..e6fe263ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ option(BUILD_EXAMPLE_GAME_OF_LIFE "Enable building examples/game_of_life" OFF) option(BUILD_EXAMPLE_CIRCLES_BRUTE_FORCE "Enable building examples/circles_brute_force" OFF) option(BUILD_EXAMPLE_CIRCLES_SPATIAL_3D "Enable building examples/circles_spatial_3d" OFF) option(BUILD_EXAMPLE_RTC_EXAMPLE "Enable building examples/rtc_example" OFF) +option(BUILD_EXAMPLE_STABLE_MATCHING "Enable building examples/stable_matching" OFF) # Option to enable/disable building the static library option(VISUALISATION "Enable visualisation support" OFF) @@ -116,6 +117,10 @@ if(BUILD_ALL_EXAMPLES OR BUILD_EXAMPLE_CIRCLES_SPATIAL_3D) add_subdirectory(examples/circles_spatial3D) endif() +if(BUILD_ALL_EXAMPLES OR BUILD_EXAMPLE_STABLE_MATCHING) + add_subdirectory(examples/stable_matching) +endif() + # Add the tests directory (if required) if(BUILD_TESTS) add_subdirectory(tests) diff --git a/examples/stable_matching/CMakeLists.txt b/examples/stable_matching/CMakeLists.txt new file mode 100644 index 000000000..c892e5f87 --- /dev/null +++ b/examples/stable_matching/CMakeLists.txt @@ -0,0 +1,40 @@ +# Set the minimum cmake version to that which supports cuda natively. +# 3.10 required for cuda -std=c++14, however 3.12 fixes some device linker errors +cmake_minimum_required(VERSION VERSION 3.12 FATAL_ERROR) + +# Name the project and set languages +project(stable_matching CUDA CXX) + +# Set the location of the ROOT flame gpu project relative to this CMakeList.txt +get_filename_component(FLAMEGPU_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../.. REALPATH) + +# Include common rules. +include(${FLAMEGPU_ROOT}/cmake/common.cmake) + +# Define output location of binary files +if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + # If top level project + SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin/${CMAKE_SYSTEM_NAME_LOWER}-x64/${CMAKE_BUILD_TYPE}/) +else() + # If called via add_subdirectory() + SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../../bin/${CMAKE_SYSTEM_NAME_LOWER}-x64/${CMAKE_BUILD_TYPE}/) +endif() + +# Prepare list of source files +# Can't do this automatically, as CMake wouldn't know when to regen (as CMakeLists.txt would be unchanged) +SET(ALL_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cu +) + +# Option to enable/disable building the static library +option(VISUALISATION "Enable visualisation support" OFF) + +# Add the executable and set required flags for the target +add_flamegpu_executable("${PROJECT_NAME}" "${ALL_SRC}" "${FLAMEGPU_ROOT}" "${PROJECT_BINARY_DIR}" TRUE) + +# Also set as startup project (if top level project) +set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY VS_STARTUP_PROJECT "${PROJECT_NAME}") + +# Set the default (visual studio) debugger configure_file +set_target_properties("${PROJECT_NAME}" PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + VS_DEBUGGER_COMMAND_ARGUMENTS "-s 10") \ No newline at end of file diff --git a/examples/stable_matching/src/main.cu b/examples/stable_matching/src/main.cu new file mode 100644 index 000000000..0a60872be --- /dev/null +++ b/examples/stable_matching/src/main.cu @@ -0,0 +1,302 @@ +#include +#include +#include +#include +#include + +#include "flamegpu/flame_api.h" +#include "flamegpu/runtime/flamegpu_api.h" +#include "flamegpu/io/factory.h" + +// @todo - support more than a single round via submodel once available +// @todo - make an issue about large agent array performance - interleaving would be better? Maybe only interleave for relatively larger array? > glm's max size? +// @todo - Add ability to move entire populations to alternate states? +// @todo - rename Agents/variables to a generic stable_matching problem rather than marriage + +// Constants +#define NOT_ENGAGED UINT_MAX + +// size of each population, which is related to the size of preference lists hence compile time definition. +#define POPULATION_SIZE 1024 + + + +FLAMEGPU_AGENT_FUNCTION_CONDITION(man_make_proposals_condition) { + // If not provisionally engaged (but not actually engaged?) + return FLAMEGPU->getVariable("engaged_to") == NOT_ENGAGED; +} +FLAMEGPU_AGENT_FUNCTION(man_make_proposals, MsgNone, MsgBruteForce ) { + uint32_t r = FLAMEGPU->getVariable("round"); + + // If the round is valid, select the next proposal and output a message. + if (r < POPULATION_SIZE) { + // Get the next preferable woman + uint32_t woman = FLAMEGPU->getVariable("preferred_woman", r); + + // Make a proposal + FLAMEGPU->message_out.setVariable("from", FLAMEGPU->getVariable("id")); + FLAMEGPU->message_out.setVariable("to", woman); + + // Update the round. + FLAMEGPU->setVariable("round", r); + } + return ALIVE; +} + + +FLAMEGPU_AGENT_FUNCTION(woman_check_proposals, MsgBruteForce, MsgNone) { + uint32_t suitor = NOT_ENGAGED; + uint32_t current_suitor_rank = FLAMEGPU->getVariable("current_suitor_rank"); + uint32_t id = FLAMEGPU->getVariable("id"); + // Iterate proposals to find the best suitor so far for this round of proposals. + for (auto &message : FLAMEGPU->message_in) { + // If the message is for the current agent, proceed + if (id == message.getVariable("to")) { + // Make sure the message is in bounds for the agent array + const uint32_t from = message.getVariable("from"); + if (from < POPULATION_SIZE) { + // Find the authors rank + const uint32_t rank = FLAMEGPU->getVariable("preferred_man", from); + + // If the rank is lower than current (or current rank is invalid, implied because UINT_MAX.) + if (rank < current_suitor_rank) { + current_suitor_rank = rank; + suitor = from; + } + } + } + } + // Update agent data + if (suitor != NOT_ENGAGED) { + FLAMEGPU->setVariable("current_suitor", suitor); + FLAMEGPU->setVariable("current_suitor_rank", current_suitor_rank); + } + return ALIVE; +} + +FLAMEGPU_AGENT_FUNCTION_CONDITION(woman_notify_suitors_condition) { + // Have a proposal to accept (i.e. provisional engagement) + return FLAMEGPU->getVariable("current_suitor") != NOT_ENGAGED; +} +FLAMEGPU_AGENT_FUNCTION(woman_notify_suitors, MsgNone, MsgBruteForce) { + // Condition means this is only called if a suitor is found. + // @todo - alternatively could just have in if statement in here / in the end of the prev function, skipping th eneed for 2 extra kernel launches... + FLAMEGPU->message_out.setVariable("from", FLAMEGPU->getVariable("id")); + FLAMEGPU->message_out.setVariable("to", FLAMEGPU->getVariable("current_suitor")); + return ALIVE; +} + +FLAMEGPU_AGENT_FUNCTION(man_check_notifications, MsgBruteForce, MsgNone) { + // Reset the engaged property + uint32_t engaged_to = NOT_ENGAGED; + + uint32_t id = FLAMEGPU->getVariable("id"); + + // Iterate messages looking for notifications of accepeted proposals. + for (auto &message : FLAMEGPU->message_in) { + // If this agent is the recipient + if (id == message.getVariable("to")) { + // Store who it is from - There should only be one message per key. + // @todo - optimise this to use a key-based message scheme with only a single message per key. + engaged_to = message.getVariable("from"); + } + } + + // Update agent data in global memory. + FLAMEGPU->setVariable("engaged_to", engaged_to); + return ALIVE; +} + +FLAMEGPU_EXIT_CONDITION(check_resolved_exit_condition) { + // If anyone is not engaged, keep going + uint32_t unengaged_count = FLAMEGPU->agent("man", "unengaged").count("engaged_to", NOT_ENGAGED); + + // uint32_t unengaged_wcount = FLAMEGPU->agent("woman", "default").count("current_suitor", NOT_ENGAGED); + // printf("Iter %u: %u unengaged men, %u unengaged women\n", FLAMEGPU->getStepCounter(), unengaged_count, unengaged_wcount); + + if (unengaged_count == 0) { + return EXIT; + } + return CONTINUE; +} + +// Exit function which will output the resolved status? +FLAMEGPU_EXIT_FUNCTION(exit_resolved_or_not) { + uint32_t unengaged_count = FLAMEGPU->agent("man", "unengaged").count("engaged_to", NOT_ENGAGED); + + // Output success / error message. + if (unengaged_count == 0) { + printf("Completed in %u iterations\n", FLAMEGPU->getStepCounter()); + } else { + printf("%u remaining unengaged pairs after %u iterations\n", unengaged_count, FLAMEGPU->getStepCounter()); + } +} + +int main(int argc, const char ** argv) { + ModelDescription model("stable_matching"); + + { // proposal message list + MsgBruteForce::Description &proposal_message = model.newMessage("proposal"); + proposal_message.newVariable("from"); + proposal_message.newVariable("to"); + } + + { // notificaiton message list + MsgBruteForce::Description ¬ification_message = model.newMessage("notification"); + notification_message.newVariable("from"); + notification_message.newVariable("to"); + } + + { // Man agent + AgentDescription &man_agent = model.newAgent("man"); + + // Declare variables + man_agent.newVariable("id"); + man_agent.newVariable("engaged_to", NOT_ENGAGED); + man_agent.newVariable("round"); + man_agent.newVariable("preferred_woman"); + + // Declare states + man_agent.newState("engaged"); + man_agent.newState("unengaged"); + + // make_proposals function. + auto& make_proposals = man_agent.newFunction("make_proposals", man_make_proposals); + make_proposals.setInitialState("unengaged"); + make_proposals.setEndState("unengaged"); + make_proposals.setFunctionCondition(man_make_proposals_condition); + make_proposals.setMessageOutput("proposal"); + + // Check notifcations from other population + auto& check_notifications = man_agent.newFunction("check_notifications", man_check_notifications); + check_notifications.setInitialState("unengaged"); + check_notifications.setEndState("unengaged"); + check_notifications.setMessageInput("notification"); + } + + { // Woman agent + AgentDescription &woman_agent = model.newAgent("woman"); + + // Declare variables + woman_agent.newVariable("id"); + woman_agent.newVariable("current_suitor", NOT_ENGAGED); + woman_agent.newVariable("current_suitor_rank", NOT_ENGAGED); + woman_agent.newVariable("preferred_man"); + + // Declare states + woman_agent.newState("default"); + + // make_proposals function. + auto& check_proposals = woman_agent.newFunction("check_proposals", woman_check_proposals); + check_proposals.setInitialState("default"); + check_proposals.setEndState("default"); + check_proposals.setMessageInput("proposal"); + + // Check notifcations from other population + auto& notify_suitors = woman_agent.newFunction("notify_suitors", woman_notify_suitors); + notify_suitors.setInitialState("default"); + notify_suitors.setEndState("default"); + notify_suitors.setMessageOutput("notification"); + notify_suitors.setFunctionCondition(woman_notify_suitors_condition); + } + + // Exit Condition and Function + { + model.addExitCondition(check_resolved_exit_condition); + model.addExitFunction(exit_resolved_or_not); + } + + + /** + * GLOBALS + */ + { + // EnvironmentDescription &env = model.Environment(); + } + + /** + * Control flow + */ + { + LayerDescription &layer = model.newLayer(); + layer.addAgentFunction("make_proposals"); + } + { + LayerDescription &layer = model.newLayer(); + layer.addAgentFunction("check_proposals"); + } + { + LayerDescription &layer = model.newLayer(); + layer.addAgentFunction("notify_suitors"); + } + { + LayerDescription &layer = model.newLayer(); + layer.addAgentFunction("check_notifications"); + } + + /** + * Create Model Runner + */ + CUDAAgentModel cuda_model(model); + + /** + * Initialisation + */ + cuda_model.initialise(argc, argv); + // If no agents were provided from disk, dynamically create the populations. + if (cuda_model.getSimulationConfig().xml_input_file.empty()) { + // Currently population has not been init, so generate an agent population on the fly. + // There must be a balanced population of men and women. + + // Prepare an array of rank values, to be shuffled for each agent. + std::array ranks; + std::iota(ranks.begin(), ranks.end(), 0); + + const uint32_t seed = cuda_model.getSimulationConfig().random_seed; + std::mt19937 rank_rng(seed); + + { + AgentPopulation pop(model.Agent("man"), POPULATION_SIZE); + for (uint32_t idx = 0; idx < POPULATION_SIZE; idx++) { + AgentInstance instance = pop.getNextInstance("unengaged"); + instance.setVariable("id", idx); + instance.setVariable("round", 0); + instance.setVariable("engaged_to", NOT_ENGAGED); + + // Shuffle the ranks + std::shuffle(ranks.begin(), ranks.end(), rank_rng); + instance.setVariable("preferred_woman", ranks); + } + cuda_model.setPopulationData(pop); + } + + { + AgentPopulation pop(model.Agent("woman"), POPULATION_SIZE); + for (uint32_t idx = 0; idx < POPULATION_SIZE; idx++) { + AgentInstance instance = pop.getNextInstance("default"); + instance.setVariable("id", idx); + instance.setVariable("current_suitor", NOT_ENGAGED); + instance.setVariable("current_suitor_rank", NOT_ENGAGED); + + // Shuffle the ranks + std::shuffle(ranks.begin(), ranks.end(), rank_rng); + instance.setVariable("preferred_man", ranks); + } + cuda_model.setPopulationData(pop); + } + + } else { + // @todo - validate that there are balanced populations with unique IDs. + } + + /** + * Execution + */ + cuda_model.simulate(); + + /** + * Export Pop + */ + // TODO + return 0; +}