diff --git a/CMakeLists.txt b/CMakeLists.txt index 2451f2740..3a3dfdfc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ if(NOT NO_EXAMPLES) option(BUILD_EXAMPLE_HOST_FUNCTIONS "Enable building examples/host_functions" OFF) option(BUILD_EXAMPLE_ENSEMBLE "Enable building examples/ensemble" OFF) option(BUILD_EXAMPLE_SUGARSCAPE "Enable building examples/sugarscape" OFF) + option(BUILD_EXAMPLE_STABLE_MATCHING "Enable building examples/stable_matching" OFF) endif() option(BUILD_SWIG_PYTHON "Enable python bindings via SWIG" OFF) @@ -172,6 +173,9 @@ endif() if(BUILD_ALL_EXAMPLES OR BUILD_EXAMPLE_SUGARSCAPE) add_subdirectory(examples/sugarscape) 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 OR BUILD_TESTS_DEV) # g++ 7 is required for c++ tests to build. diff --git a/examples/stable_matching/CMakeLists.txt b/examples/stable_matching/CMakeLists.txt new file mode 100644 index 000000000..3dc24d110 --- /dev/null +++ b/examples/stable_matching/CMakeLists.txt @@ -0,0 +1,36 @@ +# Minimum CMake version 3.18 for CUDA --std=c++17 +cmake_minimum_required(VERSION VERSION 3.18 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_BUILD_TYPE}/) +else() + # If called via add_subdirectory() + SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../../bin/${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 +) + +# 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..b00dd5db4 --- /dev/null +++ b/examples/stable_matching/src/main.cu @@ -0,0 +1,296 @@ +#include +#include +#include +#include +#include +#include + +#include "flamegpu/flamegpu.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 - 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, flamegpu::MessageNone, flamegpu::MessageBruteForce ) { + 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->getID()); + FLAMEGPU->message_out.setVariable("to", woman); + + // Update the round. + FLAMEGPU->setVariable("round", r); + } + return flamegpu::ALIVE; +} + + +FLAMEGPU_AGENT_FUNCTION(woman_check_proposals, flamegpu::MessageBruteForce, flamegpu::MessageNone) { + flamegpu::id_t suitor = NOT_ENGAGED; + uint32_t current_suitor_rank = FLAMEGPU->getVariable("current_suitor_rank"); + flamegpu::id_t id = FLAMEGPU->getID(); + // 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 flamegpu::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, flamegpu::MessageNone, flamegpu::MessageBruteForce) { + // 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->getID()); + FLAMEGPU->message_out.setVariable("to", FLAMEGPU->getVariable("current_suitor")); + return flamegpu::ALIVE; +} + +FLAMEGPU_AGENT_FUNCTION(man_check_notifications, flamegpu::MessageBruteForce, flamegpu::MessageNone) { + // Reset the engaged property + flamegpu::id_t engaged_to = NOT_ENGAGED; + + flamegpu::id_t id = FLAMEGPU->getID(); + + // 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 flamegpu::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 flamegpu::EXIT; + } + return flamegpu::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) { + flamegpu::ModelDescription model("stable_matching"); + + { // proposal message list + flamegpu::MessageBruteForce::Description &proposal_message = model.newMessage("proposal"); + proposal_message.newVariable("from"); + proposal_message.newVariable("to"); + } + + { // notificaiton message list + flamegpu::MessageBruteForce::Description ¬ification_message = model.newMessage("notification"); + notification_message.newVariable("from"); + notification_message.newVariable("to"); + } + + { // Man agent + flamegpu::AgentDescription &man_agent = model.newAgent("man"); + + // Declare variables + 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 + flamegpu::AgentDescription &woman_agent = model.newAgent("woman"); + + // Declare variables + 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 + */ + { + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(man_make_proposals); + } + { + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(woman_check_proposals); + } + { + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(woman_notify_suitors); + } + { + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(man_check_notifications); + } + + /** + * Create Model Runner + */ + flamegpu::CUDASimulation cuda_simulation(model); + + /** + * Initialisation + */ + cuda_simulation.initialise(argc, argv); + // If no agents were provided from disk, dynamically create the populations. + if (cuda_simulation.getSimulationConfig().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 uint64_t seed = cuda_simulation.getSimulationConfig().random_seed; + std::mt19937_64 rank_rng(seed); + + { + flamegpu::AgentVector population(model.Agent("man"), POPULATION_SIZE); + for (uint32_t idx = 0; idx < POPULATION_SIZE; idx++) { + flamegpu::AgentVector::Agent instance = population[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_simulation.setPopulationData(population, "unengaged"); + } + + { + flamegpu::AgentVector population(model.Agent("woman"), POPULATION_SIZE); + for (uint32_t idx = 0; idx < POPULATION_SIZE; idx++) { + flamegpu::AgentVector::Agent instance = population[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_simulation.setPopulationData(population, "default"); + } + + } else { + // @todo - validate that there are balanced populations with unique IDs. + } + + /** + * Execution + */ + cuda_simulation.simulate(); + + /** + * Export Pop + */ + // TODO + return 0; +}