CS525 Project 2 - Taste of Chicago
This project involved the use of a GPU for general purpose computation. the focus was on using NVidia CUDA to simulate a rapidly spreading epidemic in the Chicago loop: To keep the simulation as close to reality as possible, the project objective was to recreate a zombie attack during the Taste of Chicago fair.
Using the application
The application can be launched from the command line, specifying the name of a simulation configuration file as a parameter. The format of this file will be explained in one of the following sections.
Once launched, the application displays a view of a geographic area, filled with an initial set of agents.
- Clicking with the mouse left button and dragging - pans the view
- Scrolling with the mouse wheel - changes the zoom level
- [f] - moves the view to the next fighter agent. Fighters are explained further on.
- [o] - resets the view to the initial state.
- [m] - switches between a normal map view and occlusion map view.
- [+,-] - Increases or decreases the simulation time multiplier.
- [i] - switches between map panning mode and predator / fighter creation modes.

The screenshot above shows how the info panel looks like: the info panel contains some basic info about the visualization (the framerate and current view position), and lists the number of agents per class currently present in the simulation. At the bottom, a graph shows how the number of entities changes over time.
Simulation mechanics
The simulation is mainly based on a behavior-driven flocking algorithm, similar to what I implemented in one of my older projects.Agent Data
Each active agent maintains its position, direction and speed: the agent position is updated depending on speed and direction, and taking into account possible collisions against non-walkable areas. The direction taken by an agent depends on its behavior, i.e. wheter it is a civilian, zombie or policeman, plus its current status. The complete set of rules is the following.
Coordination Rule
When agents are near each other, they tend to move in the same direction. In the current implementation, this rule is enforced only on civilians, and depends also on their current amount of 'stress' (currently represented by the steer friction parameter. When a civilian is panicking or got stuck somewhere, they will be more likely to behave as similars around them.
Avoidance Rule
When agents get too near to each other, they will tend to move away. This rule avoids multiple agents sharing a space that is physically too small for them, and also simulates the natural safe distance that people tend to maintain from each other in less crowded situations. All types of agents have to follow the avoidance rule.
Collision Rule
This rule gets applied whenever an agent's movement would bring them in an unwalkable area (i.e. against a wall). The collision rule, as it it now, makes agents behave in a pretty stupid way: Whenever they hit a wall, their stress parameter increases and they will try to move in the first free direction they find, rotating clockwise. Since tre agent stress parameter increased, they will likely follow nearby agents, increasing the possibility of finding a meaningful path away from the obstacle. When lots of agents are present, their collective bahavior will be acceptably correct, despite this rule being an oversimplification of human behavior.
Predator Rule
This rule is the core of the predator (ok, zombie ^_^) behavior: when a civilian in in a predator's awareness range, they will run at them. When a predator is close enough to a civilian (the kill range), the civilian will become infected, while the predator hunger will go down for some time: the predator won't attack other civilians for a while. An infected civilian will look dead for a while, then turn into a zombie.
Fighter Rule
This rule is used to represent agents that try to actively fight zombies (for instance, armed policemend). The rule is overall pretty simple to the predator rule explained above: fighters will try to attack zombies in their awareness range, and if a zombie gets into the kill range of a fighter, it will die. After killing a zombie, fighters cannot immediately kill another one: this is used to simulate 'fighting time': different values for this parameter can be used to implement different scenarios (i.e. killing a zombie barehanded would require more time than using a machinegun)
Configuring the simulation
As mentioned before, the application accepts a configuration file as input: this file is used to configure almost every aspect of the simulation. This allows the user to use the program to run simulations on different maps, or configure different agent behaviors on the same map and see how the scenario evolves. The two following screenshots show the application running simulations on the chicago downtown area, and on a smaller, hand drawn map. The two simulation regions are extremely different in terms of size (2 by 3 kilometers for chicago downtown versus 100 x 100 meters for the small map), and allow the user to observe agent interactions at different scales.
What follows is an example configuration file, with comments explaining all of the currently supported setup parameters.
-- The main window width and height, in pixels.windowWidth 805
windowHeight 805
-- Image paths: specify the path to the occlusion map image and the main texture used as an overlay.
-- Both images must have the same size. Also, the occlusion map needs to be a 32bpp black and white image.
-- The 32bpp occlusion map requirement will probably be dropped in the future.
mapOcclusionImg ../../data/tinyMap.png
mapImg ../../data/tinyMap.png
-- The size of the region contained in the map, in meters.
mapWidth 100
mapHeight 100
-- The total number of agents contained in the simulation.
numAgents 200
-- The number of fighters, as a percentage of the total number of agents.
fighterRatio 0.05
-- Avoid distance in meters: agents closer than this distance will try to put some more space between each other.
simCfg.avoidDistance 4
-- Coordination distance in meters: this rule is applied to civilians only. Civilians closer than this distance will tend to move in group.
simCfg.coordDistance 5
-- Awareness levels in meters: they determine how much civilians, fighters and predators are aware of enemies or targets around them.
simCfg.civilianAwareness 20
simCfg.predatorAwareness 20
simCfg.fighterAwareness 100
-- Kill distance in meters: the maximum distance a predator or a fighter can kill.
simCfg.fighterKillDistance 5
simCfg.predatorKillDistance 2
-- predator and fighter grace time in seconds: this is the time fighters and predators will wait before trying to attack someone else.
simCfg.predatorGraceTime 5
simCfg.fighterGraceTime 2
-- Base agent speed in meters per second. This is the speed agents will normally move at.
simCfg.baseSpeed 1
-- Maximum civilian, predator and fighter speeds in meters per second. These are the maximum speeds agents can move
-- while 'excited' (running away from or towards something).
simCfg.civilianSpeed 5
simCfg.predatorSpeed 5
simCfg.fighterSpeed 5
-- Infection time: the time (seconds) required for an infected agent to become a predator.
-- Specify a big value (i.e. 65000) to simulate predators killing other agents directly.
-- This works because infected and dead agents work pretty much the same.
simCfg.infectionTime 5
CUDA Implementation
The CUDA implementation of the simulation is based mainly on two kernels which run sequentially: a bin update kernel that takes care of placing agent ids in the correct bins. Bins are used to optimize the main kernel execution: Inside the main kernel, each agent does not need to do a complete loop over each other agent to update its behavior: the loop is done only on agents in the same bin and in the 8 neighboring bins, speeding up execution by about 25%.Comments
Implementing the agent based simulation in CUDA proved to be challenging. Direct debugging of kernel code is not possible: running kernels in emulation mode proved
to be an exremely useful tool, since it was possible to use standard debugging tools (breakpoints, variable inspection, etc.) to analyze kernel code. This helped
in solving major issues, but due to the high parallel nature of native kernel execution and other significant differences, it was impossible to predict the behavior
of kernels just by running them in emulation mode.

Memory alignment
One of the most challenging things encountered while implementing this application was understanding how memory alignment works with CUDA. In my code, I pass an array of a custom struct data (my agent data) back and forth from the kernels. While compiling code for device execution, CUDA performs 8 byte alignment on the data. This can be seen in the previous debugger screenshot: a dummy variable has been added to the agent structure. The problem is that alignment is not performed on the host version of the structure, but the structure is still seen at the same type in host and device code: and memcopy operations are legal but they do not take the alignment into accout. Result: memcopies from and to the device will turn most of your data to garbage.
Inconsistent execution
Another quite serious problem arises when executing less-than-perfect code on a CUDA device: if a kernel does not end execution correctly, and resources are not consistently cleaned up, the CUDA device is apparently left in an unstable state: the same application may fail or run unpredicably, apparently depending on the current status of the device. Other than specifying crash-safe cleanup code into your application, I found no other solution. There apparently is no way to reset the CUDA runtime active on a device, in order to force it to perform some sort of clean up.
Additional Videos
myMigthyStats