This is the classic loop-variable closure problem in C#.
Your loop does stop at a million. 
After it stops at a million, you make some large number of calls to WriteHelloAsync(i). 
You're passing i to all of them. That lambda you're passing to Task.Run() isn't creating a new copy of i that holds the value i had at the time of that particular call to Task.Run(). They all have the same i, whose value keeps changing until the loop ends. 
On my machine here, the first number I see is well into six digits, meaning that's the value of i the first time a task actually gets around to calling WriteHelloAsync(). Very shortly thereafter, it starts doing the million-million-million thing you see. 
Try this:
static void Main(string[] args)
{
    for (int i = 0; i < 1000 * 1000; i++)
    {
        //  The scope of j is limited to the loop block.
        //  For each new iteration, there is a new j. 
        var j = i;
        Task.Run(() => WriteHelloAsync(j));
    }
    Console.ReadLine();
}
@Gigi points out that this will also work with a foreach rather than for:
foreach (var i in Enumerable.Range(0, 1000 * 1000))
{
    Task.Run(() => WriteHelloAsync(i));
}