import java.util.*

/**
 * 閉路を含まない有向グラフにおいて，特定の２点の間の最長パスを求める問題
 *
 * 一次元メモ配列の中で，直接つながっているものの間での遷移を考える
 *
 * スタート地点を dp 遷移の開始として, 自身i へと入っている ものの j 中で最大の値を持つもの + 1する
 * 次に，i から出ていった先 の全てへ順に遷移していく（キュー）
 *
 * 遷移の終了条件は？？？ 順番を考えないと重複
 *
 * ゴールt地点を dp 遷移の開始とする， スタックで幅優先探索で流入元へ自身 + + 1 or それよりも大きい場合はそのままの値を割り当てていく
 *
 * 全体として有効エッジ数の総和分だけ計算が必要になる
 *
 * dp[i]: ノード i を起点としたときの G の有効パスの長さの最大値
 *
 * グラフをトポロジカルソート（結合方向が順方向のみなるように，グラフのノードを１次元配列の中でソート）
 *
 * トポロジカルソートして，一番端の点から順に dp を更新していけば良い？（最長のものを求めるだけの場合）
 *
 * 左がなるべくsource 右がなるべくtarget となるようになっている
 *
 * 右から順に dp の値を更新していく
 *
 * (i)  値が入っていない場合は自身 + 1
 * (ii) 値が入っている場合は自身 + 1 と大きさを比較する, 大きい方の値を代入
 *
 * つながっている場合は値が入っていく，トポロジカルソート結果の末端から入れていくことで各ノードからスタート下
 *
 * 何の値を入れているのかわからないなってきた
 *
 */

fun main(args: Array<String>) {
    val (n, m) = readLine()!!.split(" ").map { it.toInt() }

    // グラフの情報 特定のノードへ入力するノードを格納
    // map を使う場合 putIfAbsent, getOrElse 方法
    // Array を使う場合 空のリストを入れる
    val graphIn = Array(n) { mutableListOf<Int>() }
    val graphOut = Array(n) { mutableListOf<Int>() }
    for (i in 0 until m) {
        val (x, y) = readLine()!!.split(" ").map { it.toInt() }
        graphIn[y - 1].add(x - 1)
        graphOut[x - 1].add(y - 1)
    }


    // stack push/pop, queue offer/poll,
    // pop, poll は 値が存在芝居場合 null を返すのに対して，removeLast, First は値が存在しない場合は NoSuchElementException を返す
    // push = addFirst(, offer = addLast
    // 常にキュー・スタックの中身があるかどうか確認して処理をする場合は add, remove を使えばよいのでは

    // トポロジカルソートを実装
    // どのノードも出力先のノードよりも前に来るように並べる

    // 訪問済みノードを管理し，親ノードがすべて訪問済みとなっているノードから順に訪問する
    // 毎回各ノードで訪問済みかどうかを for 文で確認するのはコスパよくないのでは？？
    // ↓
    // 各ノードの入力次数を管理しておく
    // 入力次数を訪問したノードから順に減じていく

    // 実装によってはノード番号とエッジ数を保持するクラスを作成しなくてはいけないがその必要はない
    // この部分の実装方針がすぐに頭に浮かぶかどうかが大事

    // 入力エッジ数が0 になったノードのみを格納する （出したときに トポロジカルソート配列に格納)
    val queue = ArrayDeque<Int>()
    val inEdges = Array(n) { graphIn[it].size }

    // 入力エッジ数が 0 になったものを格納
    for (i in 0 until inEdges.size) {
        if (inEdges[i] == 0) queue.addLast(i)
    }

    val topoSorted = mutableListOf<Int>()
    while (!queue.isEmpty()) {
        val n = queue.removeFirst()
        topoSorted.add(n)

        // n が出力している先の入力ノード次数を減じる
        for (node in graphOut[n]) {
            inEdges[node] -= 1

            // 入力次数が0 になったノードは queue に加える
            if (inEdges[node] == 0) {
                queue.addLast(node)
            }
        }
    }

    // dp の要領で最長パスを求める
    val dp = IntArray(n) { 0 }

    // ノード番号とトポロジカルソートした結果の対応表を作成
    // あまり賢くないか？？
    val rel = IntArray(n) { 0 }
    for (i in 0 until topoSorted.size) {
        rel[topoSorted[i]] = i
    }

    for ((i, n) in topoSorted.withIndex()) {
        // 結合先へ + 1
        for (j in graphOut[n]) {
            // トポロジカルソートした結果のどこに値を加えるかを知らなくてはいけない
            dp[rel[j]] = dp[i] + 1
        }
    }

    println(dp.max())
}