giacomelli / GeneticSharp

GeneticSharp is a fast, extensible, multi-platform and multithreading C# Genetic Algorithm library that simplifies the development of applications using Genetic Algorithms (GAs).
MIT License
1.27k stars 332 forks source link

Bug/Error when using ParallelTaskExecutor #40

Closed MattWolf74 closed 5 years ago

MattWolf74 commented 5 years ago

I just discovered that when running the optimizer within a Task/Tread/TPL Dataflow block with TaskExecutor set to ParallelTaskExecuter when instantiating GeneticAlgorithm, it blocks all other outside operations during the lifetime of the optimizer run. This does not happen when not setting the TaskExecutor option.

Most likely this is a bug? Is there a workaround? At the moment I can hence not run the objective function in parallel.

Can someone please help?

giacomelli commented 5 years ago

Yes, this is an expected behavior. The ParallTaskExecutor will run the fitness evaluation in parallel, but the call of GeneticAlgorithm.Start wait until the GeneticAlgorithm.Termination is reached.

One solution, if you want to run GeneticAlgorithm.Start in parallel of other operations in your application is running it on a separated thread.

Where are you using GeneticSharp? Console application? Unity3d?

MattWolf74 commented 5 years ago

That is not the behavior I am seeing. In all cases I run geneticalgorithm.start within its own thread/task. When I don't configure taskexecutor then the main thread is not blocked. But when I configure task executor the main thread completely blocks. For clarity purpose, with main thread I do not mean the thread on which geneticalgorithm.start is running. This should never be the case. Imagine you run the algorithm from a UI thread then the UI thread blocks which is in all cases highly undesirable. I ran the same tests from within a console and from wpf both with the same observation.

On Wed, Oct 3, 2018, 23:34 Diego Giacomelli notifications@github.com wrote:

Yes, this is an expected behavior. The ParallTaskExecutor will run the fitness evaluation in parallel, but the call of GeneticAlgorithm.Start wait until the GeneticAlgorithm.Termination is reached.

One solution, if you want to run GeneticAlgorithm.Start in parallel of other operations in your application is running it on a separated thread.

Where are you using GeneticSharp? Console application? Unity3d?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/giacomelli/GeneticSharp/issues/40#issuecomment-426682909, or mute the thread https://github.com/notifications/unsubscribe-auth/APznJvH8DGmkj-VywPQceZCpbv1dRCjOks5uhNkPgaJpZM4XGK71 .

giacomelli commented 5 years ago

Ok, now I'm understanding better your problem. Can you provide a simple running sample with this problem?

MattWolf74 commented 5 years ago

Yes, will send one in a few hours as I am not in front of my machine right now. Thanks for looking into this.

On Wed, Oct 3, 2018, 23:47 Diego Giacomelli notifications@github.com wrote:

Ok, now I'm understanding better your problem. Can you provide a simple running sample with this problem?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/giacomelli/GeneticSharp/issues/40#issuecomment-426687988, or mute the thread https://github.com/notifications/unsubscribe-auth/APznJtfC-qwfAn3ccFFaIURDDAk8iJJIks5uhNwWgaJpZM4XGK71 .

MattWolf74 commented 5 years ago
[ProtoContract(ImplicitFields = ImplicitFields.AllPublic), Serializable]
public class GeneticOptimizerVariable
{
    public string VariableName { get; set; }
    public int NumberDigitsPrecision { get; set; }
    public double MinimumValue { get; set; }
    public double MaximumValue { get; set; }

    public GeneticOptimizerVariable()
    { }

    public GeneticOptimizerVariable(string variableName, int numberDigitsPrecision, double minimumValue, double maximumValue)
    {
        VariableName = variableName;
        NumberDigitsPrecision = numberDigitsPrecision;
        MinimumValue = minimumValue;
        MaximumValue = maximumValue;
    }
}

[ProtoContract(ImplicitFields = ImplicitFields.AllPublic), Serializable]
public class GeneticOptimizerConfiguration
{
    public int NumberThreadsToUse { get; set; }
    public string OptimizeVariableName { get; set; }
    public List<GeneticOptimizerVariable> Variables { get; set; }

    public GeneticOptimizerConfiguration()
    { }

    public GeneticOptimizerConfiguration(string optimizeVariableName, List<GeneticOptimizerVariable> variables, int numberThreadsToUse)
    {
        OptimizeVariableName = optimizeVariableName;
        Variables = variables;
        NumberThreadsToUse = numberThreadsToUse;
    }
}

[ProtoContract(ImplicitFields = ImplicitFields.AllPublic), Serializable]
public class GeneticOptimizerResult
{
    public string OutputVariableName { get; set; }
    public List<string> InputVariableNames { get; set; }
    public List<double> BestFitInputs { get; set; }
    public double BestFitOutput { get; set; }
    public List<string> IterationArray { get; set; } //comma delimited -> values 1,...,n-1 = input values, n = output value

    public GeneticOptimizerResult()
    {
        InputVariableNames = new List<string>();
        BestFitInputs = new List<double>();
        IterationArray = new List<string>();
    }

    public GeneticOptimizerResult(string optimizationVariableName, List<string> variableNames)
    {
        OutputVariableName = optimizationVariableName;
        InputVariableNames = variableNames;
        BestFitInputs = new List<double>();
        IterationArray = new List<string>();
    }
}

public class GeneticOptimizer
{
    private const int MinimumNumberPopulation = 50;
    private const int MaximumNumberPopulation = 100;
    private readonly GeneticOptimizerConfiguration _configuration;
    private readonly Action<string> _generationRanCallback;
    private readonly GeneticAlgorithm _algorithm;
    private GeneticOptimizerResult _result;

    public GeneticOptimizer(GeneticOptimizerConfiguration configuration, Func<double[], double> objectiveFunction, Action<string> generationRanCallback = null)
    {
        //store configuration
        _configuration = configuration;
        _generationRanCallback = generationRanCallback;

        //set min/max/precision of input variables
        var minValues = new double[_configuration.Variables.Count];
        var maxValues = new double[_configuration.Variables.Count];
        var fractionDigits = new int[_configuration.Variables.Count];

        for (int index = 0; index < _configuration.Variables.Count; index++)
        {
            minValues[index] = _configuration.Variables[index].MinimumValue;
            maxValues[index] = _configuration.Variables[index].MaximumValue;
            fractionDigits[index] = _configuration.Variables[index].NumberDigitsPrecision;
        }

        //total bits
        var totalBits = new int[] { 64 };

        //chromosome
        var chromosome = new FloatingPointChromosome(minValues, maxValues, totalBits, fractionDigits);

        //population
        var population = new Population(MinimumNumberPopulation, MaximumNumberPopulation, chromosome);

        //set fitness function
        var fitnessFunction = new FuncFitness(c =>
        {
            var fc = c as FloatingPointChromosome;
            var inputs = fc.ToFloatingPoints();
            var result = objectiveFunction(inputs);

            //add to results
            if (!Double.IsNaN(result))
            {
                var list = inputs.ToList();
                list.Add(result);

                _result.IterationArray.Add(string.Join(",", list));
            }

            return result;
        });

        //other variables
        var selection = new EliteSelection();
        var crossover = new UniformCrossover(0.5f);
        var mutation = new FlipBitMutation();
        var termination = new FitnessThresholdTermination();

        _algorithm = new GeneticAlgorithm(population, fitnessFunction, selection, crossover, mutation)
        {
            Termination = termination,
        };

        //task parallelism
        var taskExecutor = new ParallelTaskExecutor();
        taskExecutor.MinThreads = 1;
        taskExecutor.MaxThreads = _configuration.NumberThreadsToUse;
        _algorithm.TaskExecutor = taskExecutor;

        //if (_configuration.NumberThreadsToUse > 1)
        //{
        //    var taskExecutor = new ParallelTaskExecutor();
        //    taskExecutor.MinThreads = 1;
        //    taskExecutor.MaxThreads = _configuration.NumberThreadsToUse;
        //    _algorithm.TaskExecutor = taskExecutor;
        //}

        //register generation ran callback
        _algorithm.GenerationRan += AlgorithmOnGenerationRan;

    }

    public void Start()
    {
        //define result
        _result = new GeneticOptimizerResult(_configuration.OptimizeVariableName, _configuration.Variables.Select(x => x.VariableName).ToList());

        //start optimizer
        _algorithm.Start();
    }

    public void Stop()
    {
        _algorithm.Stop();
    }

    public GeneticOptimizerResult GetResults()
    {
        return _result;
    }

    private void AlgorithmOnGenerationRan(object sender, EventArgs e)
    {
        var bestChromosome = _algorithm.BestChromosome as FloatingPointChromosome;
        if (bestChromosome == null || bestChromosome.Fitness == null)
            return;

        var phenotype = bestChromosome.ToFloatingPoints();

        //update results with best fit
        _result.BestFitInputs = phenotype.ToList();
        _result.BestFitOutput = bestChromosome.Fitness.Value;

        //invoke callback to update
        if (_generationRanCallback != null)
        {
            var variables = string.Join(" - ", _configuration.Variables.Select((item, index) => $"{item.VariableName} = {phenotype[index]}"));
            var updateString = $"Optimizer Generation: {_algorithm.GenerationsNumber} - Fitness: {bestChromosome.Fitness.Value} - Variables: {variables}";
            _generationRanCallback(updateString);
        }
    }
}

Then running the following code in a Console Application:

class Program
{
    static void Main(string[] args)
    {
        var progressTimer = new System.Timers.Timer(1000);
        progressTimer.AutoReset = true;
        progressTimer.Elapsed += (sender, arg) =>
        {
            //do something
            Console.WriteLine("Hello from progress timer");

        };

        //start timer
        progressTimer.Start();

        Task.Run(() =>
        {
            RunGeneticOptimizer();

        }).Wait();

        Console.WriteLine("All tasks inside actionblock completed");

        Console.WriteLine($"Press Key to quit");
        Console.ReadLine();

    }

    private static void GenerationRanCallback(string obj)
    {
        Console.WriteLine(obj);
    }

    private static void RunGeneticOptimizer()
    {
        //optimizer variables
        var variables = new List<GeneticOptimizerVariable>()
        {
            new GeneticOptimizerVariable("x", 4, -10, 10)
        };

        //optimizer configuration
        var configuration = new GeneticOptimizerConfiguration("y", variables, 1);

        //objective function
        var objectiveFunction = new Func<double[], double>(inputs =>
        {
            Thread.Sleep(1000);

            var objectiveFunctionResult = Math.Pow(inputs[0], 3) / Math.Exp(Math.Pow(inputs[0], 0.8));
            return objectiveFunctionResult;
        });

        //optimizer
        var optimizer = new GeneticOptimizer(configuration, objectiveFunction, GenerationRanCallback);

        var watch = new Stopwatch();
        watch.Start();

        optimizer.Start();

        watch.Stop();

        Console.WriteLine($"Number milliseconds: {watch.ElapsedMilliseconds}");

        Console.WriteLine($"Press Key to quit");
        Console.ReadLine();
    }
}

You will notice that the timer thread is blocked. The reason is that the GeneticAlgorithm is configured with taskExecutor = new ParallelTaskExecutor() where the MaxThreads is set to 1 (as specified in the console app). Even when requesting the algorithm to run with >1 threads initially blocks the timer thread.

giacomelli commented 5 years ago

Thanks, I will investigate that.

giacomelli commented 5 years ago

The behavior you described (and I saw with your sample code) was caused by those lines on ParallelTaskExecutor's Start method:

ThreadPool.GetMinThreads(out int minWorker, out int minIOC);
ThreadPool.SetMinThreads(MinThreads, minIOC);

ThreadPool.GetMaxThreads(out int maxWorker, out int maxIOC);
ThreadPool.SetMaxThreads(MaxThreads, maxIOC);

When the MaxThreads is set to just 1, there are no others threads to attend the progressTimer.

To solve this, I changed that lines to:

// Do not change values if the new values to min and max threads are lower than already configured on ThreadPool.
ThreadPool.GetMinThreads(out minWorker, out minIOC);

if (MinThreads > minWorker)
{
    ThreadPool.SetMinThreads(MinThreads, minIOC);
}

ThreadPool.GetMaxThreads(out maxWorker, out maxIOC);

if (MaxThreads > maxWorker)
{
    ThreadPool.SetMaxThreads(MaxThreads, maxIOC);
}

I added your sample to this branch issue-40, could you please checkout it and run the /src/Samples/Issue9Sample/Issue9Sample.sln to validate if there is no other problem about this subject?

MattWolf74 commented 5 years ago

I checked out the branch and verified that the issue is resolved. Thanks