#![allow(unused_imports)]
use std::cmp::{max, min};
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};

use nyan::*;

fn main() {
    let stdin = std::io::stdin();
    let mut sc = Scanner::new(stdin.lock());

    let n: usize = sc.read();
    let m: usize = sc.read();

    let mut g = vec![Vec::new(); n];
    for _ in 0..m {
        let x: usize = sc.read();
        let y: usize = sc.read();
        g[x - 1].push(y - 1);
    }

    // トポロジカルソート
    // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
    let t = {
        fn visit(g: &[Vec<usize>], l: &mut Vec<usize>, v: &mut [bool], i: usize) {
            if !v[i] {
                v[i] = true;
                for &j in g[i].iter() {
                    visit(g, l, v, j);
                }
                l.push(i);
            }
        }

        let mut l = Vec::new();
        let mut v = vec![false; n];
        for i in 0..n {
            if !v[i] {
                visit(&g, &mut l, &mut v, i);
            }
        }
        l
    };

    // println!("{:?}", t);

    let mut ans = 0;
    let mut d = vec![0; n];
    for i in t.into_iter().rev() {
        for &j in g[i].iter() {
            d[j] = max(d[i] + 1, d[j]);
            ans = max(ans, d[j]);
        }
    }

    // println!("{:?}", d);

    println!("{}", ans);
}

#[allow(dead_code)]
mod nyan {
    use std::cmp::Ordering;
    use std::fmt::{Debug, Display};
    use std::io;
    use std::iter::FromIterator;
    use std::str::FromStr;

    // https://doc.rust-lang.org/1.39.0/src/core/num/mod.rs.html#4473-4523
    #[inline]
    pub fn is_ascii_whitespace(c: char) -> bool {
        match c {
            '\t' | '\n' | '\x0C' | '\r' | ' ' => true,
            _ => false,
        }
    }

    #[inline]
    pub fn is_ascii_linebreak(c: char) -> bool {
        match c {
            '\n' | '\r' => true,
            _ => false,
        }
    }

    pub struct Scanner<R: io::BufRead> {
        reader: R,
        buffer: String,
        cursor: usize,
    }

    impl<R: io::BufRead> Scanner<R> {
        pub fn new(reader: R) -> Scanner<R> {
            Scanner {
                reader: reader,
                buffer: String::with_capacity(32),
                cursor: 0,
            }
        }

        fn next<F: Fn(char) -> bool>(&mut self, f: F) -> io::Result<&str> {
            loop {
                if let Some(l) = self.buffer[self.cursor..].find(|c| !is_ascii_whitespace(c)) {
                    self.cursor += l;
                    break;
                }

                self.buffer.clear();
                self.cursor = 0;
                match self.reader.read_line(&mut self.buffer) {
                    Err(e) => return Err(e),
                    Ok(0) => return Err(io::Error::from(io::ErrorKind::UnexpectedEof)),
                    _ => (),
                }
            }

            let start = self.cursor;
            self.cursor = self.buffer[self.cursor..]
                .find(f)
                .map_or(self.buffer.len(), |l| start + l);
            Ok(&self.buffer[start..self.cursor])
        }

        fn next_word(&mut self) -> io::Result<&str> {
            self.next(|c| is_ascii_whitespace(c))
        }

        fn next_line(&mut self) -> io::Result<&str> {
            self.next(|c| c == '\n' || c == '\r')
        }

        pub fn read<T>(&mut self) -> T
        where
            T: FromStr,
            <T as FromStr>::Err: Debug,
        {
            self.next_word().unwrap().parse().unwrap()
        }

        pub fn read_vec<T, U>(&mut self, n: usize) -> T
        where
            T: FromIterator<U>,
            U: FromStr,
            <U as FromStr>::Err: Debug,
        {
            (0..n).map(|_| self.read()).collect()
        }

        pub fn read_vec2d<T, U, V>(&mut self, m: usize, n: usize) -> T
        where
            T: FromIterator<U>,
            U: FromIterator<V>,
            V: FromStr,
            <V as FromStr>::Err: Debug,
        {
            (0..m).map(|_| self.read_vec(n)).collect()
        }

        pub fn read_line<T: FromIterator<char>>(&mut self) -> T {
            self.next_line().unwrap().chars().collect()
        }

        pub fn read_lines<T, U>(&mut self, n: usize) -> T
        where
            T: FromIterator<U>,
            U: FromIterator<char>,
        {
            (0..n).map(|_| self.read_line()).collect()
        }
    }

    #[test]
    fn test_scanner_next_word() {
        let buf = io::Cursor::new(b"hello world\nfoo\n\n\n\nbar\n");
        let mut r = Scanner::new(buf);

        assert_eq!(r.next_word().unwrap(), "hello");
        assert_eq!(r.next_word().unwrap(), "world");
        assert_eq!(r.next_word().unwrap(), "foo");
        assert_eq!(r.next_word().unwrap(), "bar");
        assert!(r.next_word().is_err());
        assert!(r.next_word().is_err());
    }

    #[test]
    fn test_scanner_next_line() {
        let buf = io::Cursor::new(b"hello world\nfoo\n\n\n\nbar\n");
        let mut r = Scanner::new(buf);

        assert_eq!(r.next_line().unwrap(), "hello world");
        assert_eq!(r.next_line().unwrap(), "foo");
        assert_eq!(r.next_line().unwrap(), "bar");
        assert!(r.next_line().is_err());
        assert!(r.next_line().is_err());
    }

    #[test]
    fn test_scanner_no_linebreak() {
        let buf = io::Cursor::new(b"hello world");
        let mut r = Scanner::new(buf);

        assert_eq!(r.next_word().unwrap(), "hello");
        assert_eq!(r.next_word().unwrap(), "world");
        assert!(r.next_word().is_err());
        assert!(r.next_word().is_err());
    }

    pub fn print_vec<T: Display>(v: &[T]) {
        if v.is_empty() {
            return;
        }

        print!("{}", v[0]);
        for i in v.iter().skip(1) {
            print!(" {}", i);
        }
        println!();
    }

    // C++ like binary search functions (upper_bound, lower_bound, equal_range)
    pub trait BinarySearch<T> {
        fn lower_bound(&self, value: &T) -> usize;
        fn upper_bound(&self, value: &T) -> usize;
        fn equal_range(&self, value: &T) -> (usize, usize) {
            (self.lower_bound(value), self.upper_bound(value))
        }
    }

    impl<T: Ord> BinarySearch<T> for [T] {
        // https://en.cppreference.com/w/cpp/algorithm/lower_bound#Possible_implementation
        fn lower_bound(&self, value: &T) -> usize {
            let mut first = 0;
            let mut last = self.len();
            while last != first {
                let mid = (first + last) / 2;
                match self[mid].cmp(value) {
                    Ordering::Less => first = mid + 1,
                    _ => last = mid,
                }
            }
            first
        }

        // https://en.cppreference.com/w/cpp/algorithm/upper_bound#Possible_implementation
        fn upper_bound(&self, value: &T) -> usize {
            let mut first = 0;
            let mut last = self.len();
            while last != first {
                let mid = (first + last) / 2;
                match self[mid].cmp(value) {
                    Ordering::Greater => last = mid,
                    _ => first = mid + 1,
                }
            }
            first
        }
    }

    #[test]
    pub fn test_binary_search() {
        let data = vec![1, 1, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6];
        assert_eq!(data.lower_bound(&4), 7);
        assert_eq!(data.upper_bound(&4), 10);
    }

    // https://doc.rust-lang.org/std/cmp/struct.Reverse.html
    #[derive(PartialEq, Eq, Debug)]
    pub struct Rev<T>(pub T);

    impl<T: PartialOrd> PartialOrd for Rev<T> {
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
            other.0.partial_cmp(&self.0)
        }
    }

    impl<T: Ord> Ord for Rev<T> {
        fn cmp(&self, other: &Self) -> Ordering {
            other.0.cmp(&self.0)
        }
    }

    // Extended Euclidean algorithm
    // ax + by = gcd(x, y)
    pub fn extended_gcd(a: u64, b: u64) -> (u64, i64, i64) {
        if b == 0 {
            (a, 1, 0)
        } else {
            let (m, y, x) = extended_gcd(b, a % b);
            (m, x, y - (a / b) as i64 * x)
        }
    }

    pub fn gcd(a: u64, b: u64) -> u64 {
        if b == 0 {
            a
        } else {
            gcd(b, a % b)
        }
    }

    pub fn lcm(a: u64, b: u64) -> u64 {
        (a * b) / gcd(a, b)
    }

    // x^n (mod p)
    pub fn pow_mod(mut x: u64, mut n: u64, p: u64) -> u64 {
        let mut r = 1;
        while n > 0 {
            if n & 1 > 0 {
                r *= x;
                r %= p;
            }
            x *= x;
            x %= p;
            n >>= 1;
        }
        r
    }

    #[test]
    fn test_pow_mod() {
        assert_eq!(pow_mod(4, 13, 497), 445);
        assert_eq!(pow_mod(2, 90, 13), 12);
        assert_eq!(pow_mod(2, 99999999, 147), 134);
    }

    // a^{p-1} = 1 (mod p) (Fermat's little theorem)
    // a^{p - 2} * a = 1 → a^{-1} = a^{p-2}
    pub fn inv_mod(n: u64, p: u64) -> u64 {
        pow_mod(n, p - 2, p)
    }

    // s * (s + 1) * .. * t (mod p)
    pub fn prod_mod(s: u64, t: u64, p: u64) -> u64 {
        (s..t + 1).fold(1, |acc, x| acc * x % p)
    }

    // nCk (mod p)
    pub fn comb_mod(n: u64, k: u64, p: u64) -> u64 {
        // nCk = n! / (k! * (n - k)!)
        //     = ((n-k+1) * (n-k+2) * ... * n) * inv(k!)
        prod_mod(n - k + 1, n, p) * inv_mod(prod_mod(1, k, p), p) % p
    }

    #[inline]
    pub fn minmax<T: Ord>(a: T, b: T) -> (T, T) {
        if a < b {
            (a, b)
        } else {
            (b, a)
        }
    }

    #[test]
    fn test_minmax() {
        assert_eq!(minmax(1, 3), (1, 3));
        assert_eq!(minmax(3, 1), (1, 3));
    }

    #[inline]
    pub fn digits(x: u64, base: u64) -> u32 {
        (1..)
            .find(|&i| base.pow(i - 1) <= x && x < base.pow(i))
            .unwrap_or(0)
    }

    #[test]
    fn test_digits() {
        assert_eq!(digits(1234, 10), 4);
        assert_eq!(digits(1234567890, 10), 10);
        assert_eq!(digits(0xdeadbeef, 16), 8);
        assert_eq!(digits(0o123456, 8), 6);
        assert_eq!(digits(0b10101, 2), 5);
    }

    pub struct UnionFind {
        n: usize,
        root: Vec<usize>,
        rank: Vec<usize>,
        size: Vec<usize>,
    }

    impl UnionFind {
        pub fn new(n: usize) -> UnionFind {
            UnionFind {
                n: n,
                root: (0..n).collect(),
                rank: vec![0; n],
                size: vec![1; n],
            }
        }

        pub fn root(&mut self, x: usize) -> usize {
            let r = self.root[x];
            if r == x {
                r
            } else {
                self.root[x] = self.root(r);
                self.root[x]
            }
        }

        pub fn merge(&mut self, x: usize, y: usize) {
            let rx = self.root(x);
            let ry = self.root(y);
            if rx == ry {
                return;
            }

            if self.rank[rx] < self.rank[ry] {
                self.root[rx] = ry;
                self.size[ry] += self.size[rx];
            } else {
                self.root[ry] = rx;
                self.size[rx] += self.size[ry];
                if self.rank[rx] == self.rank[ry] {
                    self.rank[rx] += 1;
                }
            }
        }

        pub fn size(&mut self, x: usize) -> usize {
            let rx = self.root(x);
            self.size[rx]
        }
    }
}


Measured memory_bytes: 15868